diff --git a/public/.well-known/security.txt b/public/.well-known/security.txt deleted file mode 100644 index 526ccf81..00000000 --- a/public/.well-known/security.txt +++ /dev/null @@ -1,36 +0,0 @@ -# Security Policy - -Contact: mailto:security@agenticgovernance.digital -Expires: 2026-10-09T00:00:00.000Z -Preferred-Languages: en -Canonical: https://agenticgovernance.digital/.well-known/security.txt - -# Encryption -# Please use PGP encryption for sensitive security reports -# Public key available at: https://agenticgovernance.digital/.well-known/pgp-key.txt - -# Policy -# We take security seriously and appreciate responsible disclosure -# Please allow up to 48 hours for initial response -# We aim to patch critical vulnerabilities within 7 days - -# Scope -# In scope: -# - XSS, CSRF, SQL/NoSQL injection -# - Authentication/authorization bypass -# - Sensitive data exposure -# - Server-side vulnerabilities - -# Out of scope: -# - Social engineering -# - Physical security -# - Denial of Service (DoS/DDoS) -# - Self-XSS -# - Clickjacking on pages without sensitive actions - -# Acknowledgments -# https://agenticgovernance.digital/security-researchers - -# Hall of Fame -# Security researchers who responsibly disclosed vulnerabilities: -# (None yet - be the first!) diff --git a/public/docs/research-governance-roi-case-study.pdf b/public/docs/research-governance-roi-case-study.pdf deleted file mode 100644 index 7900466e..00000000 Binary files a/public/docs/research-governance-roi-case-study.pdf and /dev/null differ diff --git a/public/images/tractatus-icon-new.svg b/public/images/tractatus-icon-new.svg deleted file mode 100644 index c41e3123..00000000 --- a/public/images/tractatus-icon-new.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - T - diff --git a/public/images/tractatus-icon.svg b/public/images/tractatus-icon.svg deleted file mode 100644 index ad0423d3..00000000 --- a/public/images/tractatus-icon.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/js/admin/newsletter-management.js b/public/js/admin/newsletter-management.js deleted file mode 100644 index 10524914..00000000 --- a/public/js/admin/newsletter-management.js +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Newsletter Management - Admin Interface - */ - -let currentPage = 1; -const perPage = 50; -let currentFilters = { - status: 'active', - verified: 'all' -}; - -/** - * Initialize page - */ -async function init() { - // Event listeners (navbar handles admin name and logout now) - document.getElementById('refresh-btn').addEventListener('click', () => loadAll()); - document.getElementById('export-btn').addEventListener('click', exportSubscribers); - document.getElementById('filter-status').addEventListener('change', handleFilterChange); - document.getElementById('filter-verified').addEventListener('change', handleFilterChange); - document.getElementById('prev-page').addEventListener('click', () => changePage(-1)); - document.getElementById('next-page').addEventListener('click', () => changePage(1)); - - // Load data - await loadAll(); -} - -/** - * Load all data - */ -async function loadAll() { - await Promise.all([ - loadStats(), - loadSubscribers() - ]); -} - -/** - * Load statistics - */ -async function loadStats() { - try { - const token = localStorage.getItem('admin_token'); - const response = await fetch('/api/newsletter/admin/stats', { - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) throw new Error('Failed to load stats'); - - const data = await response.json(); - const stats = data.stats; - - document.getElementById('stat-total').textContent = stats.total || 0; - document.getElementById('stat-active').textContent = stats.active || 0; - document.getElementById('stat-verified').textContent = stats.verified || 0; - document.getElementById('stat-recent').textContent = stats.recent_30_days || 0; - } catch (error) { - console.error('Error loading stats:', error); - } -} - -/** - * Load subscribers list - */ -async function loadSubscribers() { - try { - const token = localStorage.getItem('admin_token'); - const skip = (currentPage - 1) * perPage; - - const params = new URLSearchParams({ - limit: perPage, - skip, - active: currentFilters.status === 'all' ? null : currentFilters.status === 'active', - verified: currentFilters.verified === 'all' ? null : currentFilters.verified === 'verified' - }); - - // Remove null values - for (const [key, value] of [...params.entries()]) { - if (value === 'null' || value === null) { - params.delete(key); - } - } - - const response = await fetch(`/api/newsletter/admin/subscriptions?${params}`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) throw new Error('Failed to load subscribers'); - - const data = await response.json(); - renderSubscribers(data.subscriptions); - updatePagination(data.pagination); - } catch (error) { - console.error('Error loading subscribers:', error); - document.getElementById('subscribers-table').innerHTML = ` - - - Error loading subscribers. Please refresh the page. - - - `; - } -} - -/** - * Render subscribers table - */ -function renderSubscribers(subscriptions) { - const tbody = document.getElementById('subscribers-table'); - - if (!subscriptions || subscriptions.length === 0) { - tbody.innerHTML = ` - - - No subscribers found - - - `; - return; - } - - tbody.innerHTML = subscriptions.map(sub => ` - - - ${escapeHtml(sub.email)} - - - ${escapeHtml(sub.name) || '-'} - - - ${escapeHtml(sub.source) || 'unknown'} - - - ${sub.active - ? ` - ${sub.verified ? '' : ''} - ${sub.verified ? 'Active ✓' : 'Active'} - ` - : 'Inactive' - } - - - ${formatDate(sub.subscribed_at)} - - - - - - - `).join(''); - - // Add event listeners to buttons - tbody.querySelectorAll('.view-details-btn').forEach(btn => { - btn.addEventListener('click', () => viewDetails(btn.dataset.id)); - }); - - tbody.querySelectorAll('.delete-subscriber-btn').forEach(btn => { - btn.addEventListener('click', () => deleteSubscriber(btn.dataset.id, btn.dataset.email)); - }); -} - -/** - * Update pagination UI - */ -function updatePagination(pagination) { - document.getElementById('showing-from').textContent = pagination.skip + 1; - document.getElementById('showing-to').textContent = Math.min(pagination.skip + pagination.limit, pagination.total); - document.getElementById('total-count').textContent = pagination.total; - - document.getElementById('prev-page').disabled = currentPage === 1; - document.getElementById('next-page').disabled = !pagination.has_more; -} - -/** - * Handle filter change - */ -function handleFilterChange() { - currentFilters.status = document.getElementById('filter-status').value; - currentFilters.verified = document.getElementById('filter-verified').value; - currentPage = 1; - loadSubscribers(); -} - -/** - * Change page - */ -function changePage(direction) { - currentPage += direction; - loadSubscribers(); -} - -/** - * View subscriber details - */ -async function viewDetails(id) { - alert(`Subscriber details for ID: ${id}\n(Full implementation would show a modal with complete subscriber information)`); -} - -/** - * Delete subscriber - */ -async function deleteSubscriber(id, email) { - if (!confirm(`Are you sure you want to delete subscription for ${email}?\n\nThis action cannot be undone.`)) { - return; - } - - try { - const token = localStorage.getItem('admin_token'); - const response = await fetch(`/api/newsletter/admin/subscriptions/${id}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) throw new Error('Failed to delete subscriber'); - - alert('Subscriber deleted successfully'); - await loadAll(); - } catch (error) { - console.error('Error deleting subscriber:', error); - alert('Failed to delete subscriber. Please try again.'); - } -} - -/** - * Export subscribers as CSV - */ -async function exportSubscribers() { - try { - const token = localStorage.getItem('admin_token'); - const active = currentFilters.status === 'all' ? 'all' : 'true'; - - const response = await fetch(`/api/newsletter/admin/export?active=${active}`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (!response.ok) throw new Error('Failed to export subscribers'); - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `newsletter-subscribers-${Date.now()}.csv`; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - console.error('Error exporting subscribers:', error); - alert('Failed to export subscribers. Please try again.'); - } -} - -// Logout handled by navbar component - -/** - * Format date - */ -function formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); -} - -/** - * Escape HTML - */ -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -// Initialize on page load -document.addEventListener('DOMContentLoaded', init); diff --git a/public/js/blog-post.js b/public/js/blog-post.js deleted file mode 100644 index 0ecc923a..00000000 --- a/public/js/blog-post.js +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Blog Post Page - Client-Side Logic - * Handles fetching and displaying individual blog posts with metadata, sharing, and related posts - */ - -let currentPost = null; - -/** - * Initialize the blog post page - */ -async function init() { - try { - // Get slug from URL parameter - const urlParams = new URLSearchParams(window.location.search); - const slug = urlParams.get('slug'); - - if (!slug) { - showError('No blog post specified'); - return; - } - - await loadPost(slug); - } catch (error) { - console.error('Error initializing blog post:', error); - showError('Failed to load blog post'); - } -} - -/** - * Load blog post by slug - */ -async function loadPost(slug) { - try { - const response = await fetch(`/api/blog/${slug}`); - const data = await response.json(); - - if (!data.success) { - throw new Error(data.error || 'Post not found'); - } - - currentPost = data.post; - - // Render post - renderPost(); - - // Load related posts - loadRelatedPosts(); - - // Attach event listeners - attachEventListeners(); - } catch (error) { - console.error('Error loading post:', error); - showError(error.message || 'Post not found'); - } -} - -/** - * Helper: Safely set element content - */ -function safeSetContent(elementId, content) { - const element = document.getElementById(elementId); - if (element) { - element.textContent = content; - return true; - } - return false; -} - -/** - * Helper: Safely set element attribute - */ -function safeSetAttribute(elementId, attribute, value) { - const element = document.getElementById(elementId); - if (element) { - element.setAttribute(attribute, value); - return true; - } - return false; -} - -/** - * Render the blog post - */ -function renderPost() { - // Hide loading state - safeSetClass('loading-state', 'add', 'hidden'); - safeSetClass('error-state', 'add', 'hidden'); - - // Show post content - safeSetClass('post-content', 'remove', 'hidden'); - - // Update page title and meta description - safeSetContent('page-title', `${currentPost.title} | Tractatus Blog`); - safeSetAttribute('page-description', 'content', currentPost.excerpt || currentPost.title); - - // Update social media meta tags - updateSocialMetaTags(currentPost); - - // Update breadcrumb - safeSetContent('breadcrumb-title', truncate(currentPost.title, 50)); - - // Render post header - if (currentPost.category) { - safeSetContent('post-category', currentPost.category); - } else { - const categoryEl = document.getElementById('post-category'); - if (categoryEl) categoryEl.style.display = 'none'; - } - - safeSetContent('post-title', currentPost.title); - - // Author - handle both flat (author_name) and nested (author.name) structures - const authorName = currentPost.author_name || currentPost.author?.name || 'Tractatus Team'; - safeSetContent('post-author', authorName); - - // Date - const publishedDate = new Date(currentPost.published_at); - const formattedDate = publishedDate.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }); - safeSetContent('post-date', formattedDate); - safeSetAttribute('post-date', 'datetime', currentPost.published_at); - - // Read time - const wordCount = currentPost.content ? currentPost.content.split(/\s+/).length : 0; - const readTime = Math.max(1, Math.ceil(wordCount / 200)); - safeSetContent('post-read-time', `${readTime} min read`); - - // Tags - if (currentPost.tags && currentPost.tags.length > 0) { - const tagsHTML = currentPost.tags.map(tag => ` - - ${escapeHtml(tag)} - - `).join(''); - const tagsEl = document.getElementById('post-tags'); - if (tagsEl) tagsEl.innerHTML = tagsHTML; - safeSetClass('post-tags-container', 'remove', 'hidden'); - } - - // AI disclosure (if AI-assisted) - if (currentPost.ai_assisted || currentPost.metadata?.ai_assisted) { - safeSetClass('ai-disclosure', 'remove', 'hidden'); - } - - // Post body - const bodyHTML = currentPost.content_html || convertMarkdownToHTML(currentPost.content); - const bodyEl = document.getElementById('post-body'); - if (bodyEl) bodyEl.innerHTML = bodyHTML; -} - -/** - * Helper: Safely add/remove class - */ -function safeSetClass(elementId, action, className) { - const element = document.getElementById(elementId); - if (element) { - if (action === 'add') { - element.classList.add(className); - } else if (action === 'remove') { - element.classList.remove(className); - } - return true; - } - return false; -} - -/** - * Load related posts (same category or similar tags) - */ -async function loadRelatedPosts() { - try { - // Fetch all published posts - const response = await fetch('/api/blog'); - const data = await response.json(); - - if (!data.success) return; - - let allPosts = data.posts || []; - - // Filter out current post - allPosts = allPosts.filter(post => post._id !== currentPost._id); - - // Find related posts (same category, or matching tags) - let relatedPosts = []; - - // Priority 1: Same category - if (currentPost.category) { - relatedPosts = allPosts.filter(post => post.category === currentPost.category); - } - - // Priority 2: Matching tags (if not enough from same category) - if (relatedPosts.length < 3 && currentPost.tags && currentPost.tags.length > 0) { - const tagMatches = allPosts.filter(post => { - if (!post.tags || post.tags.length === 0) return false; - return post.tags.some(tag => currentPost.tags.includes(tag)); - }); - relatedPosts = [...new Set([...relatedPosts, ...tagMatches])]; - } - - // Priority 3: Most recent posts (if still not enough) - if (relatedPosts.length < 3) { - const recentPosts = allPosts - .sort((a, b) => new Date(b.published_at) - new Date(a.published_at)) - .slice(0, 3); - relatedPosts = [...new Set([...relatedPosts, ...recentPosts])]; - } - - // Limit to 2-3 related posts - relatedPosts = relatedPosts.slice(0, 2); - - if (relatedPosts.length > 0) { - renderRelatedPosts(relatedPosts); - } - } catch (error) { - console.error('Error loading related posts:', error); - // Silently fail - related posts are not critical - } -} - -/** - * Render related posts section - */ -function renderRelatedPosts(posts) { - const relatedPostsHTML = posts.map(post => { - const publishedDate = new Date(post.published_at); - const formattedDate = publishedDate.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }); - - return ` -
- - ${post.featured_image ? ` -
- ${escapeHtml(post.title)} -
- ` : ` -
- - - -
- `} -
- ${post.category ? ` - - ${escapeHtml(post.category)} - - ` : ''} -

- ${escapeHtml(post.title)} -

-
- -
-
-
-
- `; - }).join(''); - - document.getElementById('related-posts').innerHTML = relatedPostsHTML; - document.getElementById('related-posts-section').classList.remove('hidden'); -} - -/** - * Attach event listeners for sharing and interactions - */ -function attachEventListeners() { - // Share on Twitter - const shareTwitterBtn = document.getElementById('share-twitter'); - if (shareTwitterBtn) { - shareTwitterBtn.addEventListener('click', () => { - const url = encodeURIComponent(window.location.href); - const text = encodeURIComponent(currentPost.title); - window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=550,height=420'); - }); - } - - // Share on LinkedIn - const shareLinkedInBtn = document.getElementById('share-linkedin'); - if (shareLinkedInBtn) { - shareLinkedInBtn.addEventListener('click', () => { - const url = encodeURIComponent(window.location.href); - window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=550,height=420'); - }); - } - - // Copy link - const copyLinkBtn = document.getElementById('copy-link'); - if (copyLinkBtn) { - copyLinkBtn.addEventListener('click', async () => { - try { - await navigator.clipboard.writeText(window.location.href); - // Show temporary success message - const originalHTML = copyLinkBtn.innerHTML; - copyLinkBtn.innerHTML = ` - - - - Copied! - `; - copyLinkBtn.classList.add('bg-green-600'); - copyLinkBtn.classList.remove('bg-gray-600'); - - setTimeout(() => { - copyLinkBtn.innerHTML = originalHTML; - copyLinkBtn.classList.remove('bg-green-600'); - copyLinkBtn.classList.add('bg-gray-600'); - }, 2000); - } catch (err) { - console.error('Failed to copy link:', err); - // Show error in button - const originalHTML = copyLinkBtn.innerHTML; - copyLinkBtn.innerHTML = ` - - - - Failed - `; - copyLinkBtn.classList.add('bg-red-600'); - copyLinkBtn.classList.remove('bg-gray-600'); - - setTimeout(() => { - copyLinkBtn.innerHTML = originalHTML; - copyLinkBtn.classList.remove('bg-red-600'); - copyLinkBtn.classList.add('bg-gray-600'); - }, 2000); - } - }); - } -} - -/** - * Show error state - */ -function showError(message) { - document.getElementById('loading-state').classList.add('hidden'); - document.getElementById('post-content').classList.add('hidden'); - - const errorStateEl = document.getElementById('error-state'); - errorStateEl.classList.remove('hidden'); - - const errorMessageEl = document.getElementById('error-message'); - if (errorMessageEl) { - errorMessageEl.textContent = message; - } -} - -/** - * Convert markdown to HTML (basic implementation - can be enhanced with a library) - */ -function convertMarkdownToHTML(markdown) { - if (!markdown) return ''; - - let html = markdown; - - // Headers - html = html.replace(/^### (.*$)/gim, '

$1

'); - html = html.replace(/^## (.*$)/gim, '

$1

'); - html = html.replace(/^# (.*$)/gim, '

$1

'); - - // Bold - html = html.replace(/\*\*(.*?)\*\*/g, '$1'); - - // Italic - html = html.replace(/\*(.*?)\*/g, '$1'); - - // Links - html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); - - // Paragraphs - html = html.replace(/\n\n/g, '

'); - html = `

${ html }

`; - - // Line breaks - html = html.replace(/\n/g, '
'); - - return html; -} - -/** - * Update social media meta tags for sharing - */ -function updateSocialMetaTags(post) { - const currentUrl = window.location.href; - const excerpt = post.excerpt || post.title; - const imageUrl = post.featured_image || 'https://agenticgovernance.digital/images/tractatus-icon.svg'; - const authorName = post.author_name || post.author?.name || 'Tractatus Team'; - - // Open Graph tags - document.getElementById('og-title').setAttribute('content', post.title); - document.getElementById('og-description').setAttribute('content', excerpt); - document.getElementById('og-url').setAttribute('content', currentUrl); - document.getElementById('og-image').setAttribute('content', imageUrl); - - if (post.published_at) { - document.getElementById('article-published-time').setAttribute('content', post.published_at); - } - document.getElementById('article-author').setAttribute('content', authorName); - - // Twitter Card tags - document.getElementById('twitter-title').setAttribute('content', post.title); - document.getElementById('twitter-description').setAttribute('content', excerpt); - document.getElementById('twitter-image').setAttribute('content', imageUrl); - document.getElementById('twitter-image-alt').setAttribute('content', `${post.title} - Tractatus AI Safety Framework`); -} - -/** - * Escape HTML to prevent XSS - */ -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -/** - * Truncate text to specified length - */ -function truncate(text, maxLength) { - if (!text || text.length <= maxLength) return text; - return `${text.substring(0, maxLength) }...`; -} - -// Initialize on page load -document.addEventListener('DOMContentLoaded', init); diff --git a/public/js/blog.js b/public/js/blog.js deleted file mode 100644 index ca82d788..00000000 --- a/public/js/blog.js +++ /dev/null @@ -1,619 +0,0 @@ -/** - * Blog Listing Page - Client-Side Logic - * Handles fetching, filtering, searching, sorting, and pagination of blog posts - */ - -// State management -let allPosts = []; -let filteredPosts = []; -let currentPage = 1; -const postsPerPage = 9; - -// Filter state -const activeFilters = { - search: '', - category: '', - sort: 'date-desc' -}; - -/** - * Initialize the blog page - */ -async function init() { - try { - await loadPosts(); - attachEventListeners(); - } catch (error) { - console.error('Error initializing blog:', error); - showError('Failed to load blog posts. Please refresh the page.'); - } -} - -/** - * Load all published blog posts from API - */ -async function loadPosts() { - try { - const response = await fetch('/api/blog'); - const data = await response.json(); - - if (!data.success) { - throw new Error(data.error || 'Failed to load posts'); - } - - allPosts = data.posts || []; - filteredPosts = [...allPosts]; - - // Apply initial sorting - sortPosts(); - - // Render initial view - renderPosts(); - updateResultsCount(); - } catch (error) { - console.error('Error loading posts:', error); - showError('Failed to load blog posts'); - } -} - -/** - * Render blog posts grid - */ -function renderPosts() { - const gridEl = document.getElementById('blog-grid'); - const emptyStateEl = document.getElementById('empty-state'); - - if (filteredPosts.length === 0) { - gridEl.innerHTML = ''; - emptyStateEl.classList.remove('hidden'); - document.getElementById('pagination').classList.add('hidden'); - return; - } - - emptyStateEl.classList.add('hidden'); - - // Calculate pagination - const startIndex = (currentPage - 1) * postsPerPage; - const endIndex = startIndex + postsPerPage; - const postsToShow = filteredPosts.slice(startIndex, endIndex); - - // Render posts - const postsHTML = postsToShow.map(post => renderPostCard(post)).join(''); - gridEl.innerHTML = postsHTML; - - // Render pagination - renderPagination(); - - // Scroll to top when changing pages (except initial load) - if (currentPage > 1) { - window.scrollTo({ top: 0, behavior: 'smooth' }); - } -} - -/** - * Render a single post card - */ -function renderPostCard(post) { - const publishedDate = new Date(post.published_at); - const formattedDate = publishedDate.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }); - - // Calculate read time (rough estimate: 200 words per minute) - const wordCount = post.content ? post.content.split(/\s+/).length : 0; - const readTime = Math.max(1, Math.ceil(wordCount / 200)); - - // Truncate excerpt to 150 characters - const excerpt = post.excerpt ? - (post.excerpt.length > 150 ? `${post.excerpt.substring(0, 150) }...` : post.excerpt) : - 'Read more...'; - - // Get category color - const categoryColor = getCategoryColor(post.category); - - return ` -
- - ${post.featured_image ? ` -
- ${escapeHtml(post.title)} -
- ` : ` -
- - - -
- `} - -
- - ${post.category ? ` - - ${escapeHtml(post.category)} - - ` : ''} - - -

- ${escapeHtml(post.title)} -

- - -

- ${escapeHtml(excerpt)} -

- - -
-
- - - - -
-
- - - - ${readTime} min read -
-
- - - ${post.tags && post.tags.length > 0 ? ` -
- ${post.tags.slice(0, 3).map(tag => ` - - ${escapeHtml(tag)} - - `).join('')} - ${post.tags.length > 3 ? `+${post.tags.length - 3} more` : ''} -
- ` : ''} -
-
-
- `; -} - -/** - * Get category color gradient - */ -function getCategoryColor(category) { - const colorMap = { - 'Framework Updates': 'from-blue-400 to-blue-600', - 'Case Studies': 'from-purple-400 to-purple-600', - 'Research': 'from-green-400 to-green-600', - 'Implementation': 'from-yellow-400 to-yellow-600', - 'Community': 'from-pink-400 to-pink-600' - }; - return colorMap[category] || 'from-gray-400 to-gray-600'; -} - -/** - * Render pagination controls - */ -function renderPagination() { - const paginationEl = document.getElementById('pagination'); - const totalPages = Math.ceil(filteredPosts.length / postsPerPage); - - if (totalPages <= 1) { - paginationEl.classList.add('hidden'); - return; - } - - paginationEl.classList.remove('hidden'); - - const prevBtn = document.getElementById('prev-page'); - const nextBtn = document.getElementById('next-page'); - const pageNumbersEl = document.getElementById('page-numbers'); - - // Update prev/next buttons - prevBtn.disabled = currentPage === 1; - nextBtn.disabled = currentPage === totalPages; - - // Render page numbers - let pageNumbersHTML = ''; - const maxVisiblePages = 5; - let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2)); - const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); - - // Adjust start if we're near the end - if (endPage - startPage < maxVisiblePages - 1) { - startPage = Math.max(1, endPage - maxVisiblePages + 1); - } - - // First page + ellipsis - if (startPage > 1) { - pageNumbersHTML += ` - - ${startPage > 2 ? '...' : ''} - `; - } - - // Visible page numbers - for (let i = startPage; i <= endPage; i++) { - const isActive = i === currentPage; - pageNumbersHTML += ` - - `; - } - - // Ellipsis + last page - if (endPage < totalPages) { - pageNumbersHTML += ` - ${endPage < totalPages - 1 ? '...' : ''} - - `; - } - - pageNumbersEl.innerHTML = pageNumbersHTML; -} - -/** - * Apply filters and search - */ -function applyFilters() { - // Reset to first page when filters change - currentPage = 1; - - // Start with all posts - filteredPosts = [...allPosts]; - - // Apply search - if (activeFilters.search) { - const searchLower = activeFilters.search.toLowerCase(); - filteredPosts = filteredPosts.filter(post => { - return ( - post.title.toLowerCase().includes(searchLower) || - (post.content && post.content.toLowerCase().includes(searchLower)) || - (post.excerpt && post.excerpt.toLowerCase().includes(searchLower)) || - (post.tags && post.tags.some(tag => tag.toLowerCase().includes(searchLower))) - ); - }); - } - - // Apply category filter - if (activeFilters.category) { - filteredPosts = filteredPosts.filter(post => post.category === activeFilters.category); - } - - // Sort - sortPosts(); - - // Update UI - renderPosts(); - updateResultsCount(); - updateActiveFiltersDisplay(); -} - -/** - * Sort posts based on active sort option - */ -function sortPosts() { - switch (activeFilters.sort) { - case 'date-desc': - filteredPosts.sort((a, b) => new Date(b.published_at) - new Date(a.published_at)); - break; - case 'date-asc': - filteredPosts.sort((a, b) => new Date(a.published_at) - new Date(b.published_at)); - break; - case 'title-asc': - filteredPosts.sort((a, b) => a.title.localeCompare(b.title)); - break; - case 'title-desc': - filteredPosts.sort((a, b) => b.title.localeCompare(a.title)); - break; - } -} - -/** - * Update results count display - */ -function updateResultsCount() { - const countEl = document.getElementById('post-count'); - if (countEl) { - countEl.textContent = filteredPosts.length; - } -} - -/** - * Update active filters display - */ -function updateActiveFiltersDisplay() { - const activeFiltersEl = document.getElementById('active-filters'); - const filterTagsEl = document.getElementById('filter-tags'); - - const hasActiveFilters = activeFilters.search || activeFilters.category; - - if (!hasActiveFilters) { - activeFiltersEl.classList.add('hidden'); - return; - } - - activeFiltersEl.classList.remove('hidden'); - - let tagsHTML = ''; - - if (activeFilters.search) { - tagsHTML += ` - - Search: "${escapeHtml(activeFilters.search)}" - - - `; - } - - if (activeFilters.category) { - tagsHTML += ` - - Category: ${escapeHtml(activeFilters.category)} - - - `; - } - - filterTagsEl.innerHTML = tagsHTML; -} - -/** - * Clear all filters - */ -function clearFilters() { - activeFilters.search = ''; - activeFilters.category = ''; - - // Reset UI elements - document.getElementById('search-input').value = ''; - document.getElementById('category-filter').value = ''; - - // Reapply filters - applyFilters(); -} - -/** - * Remove specific filter - */ -function removeFilter(filterType) { - if (filterType === 'search') { - activeFilters.search = ''; - document.getElementById('search-input').value = ''; - } else if (filterType === 'category') { - activeFilters.category = ''; - document.getElementById('category-filter').value = ''; - } - - applyFilters(); -} - -/** - * Attach event listeners - */ -function attachEventListeners() { - // Search input (debounced) - const searchInput = document.getElementById('search-input'); - let searchTimeout; - searchInput.addEventListener('input', e => { - clearTimeout(searchTimeout); - searchTimeout = setTimeout(() => { - activeFilters.search = e.target.value.trim(); - applyFilters(); - }, 300); - }); - - // Category filter - const categoryFilter = document.getElementById('category-filter'); - categoryFilter.addEventListener('change', e => { - activeFilters.category = e.target.value; - applyFilters(); - }); - - // Sort select - const sortSelect = document.getElementById('sort-select'); - sortSelect.addEventListener('change', e => { - activeFilters.sort = e.target.value; - applyFilters(); - }); - - // Clear filters button - const clearFiltersBtn = document.getElementById('clear-filters'); - clearFiltersBtn.addEventListener('click', clearFilters); - - // Pagination - prev/next buttons - const prevBtn = document.getElementById('prev-page'); - const nextBtn = document.getElementById('next-page'); - - prevBtn.addEventListener('click', () => { - if (currentPage > 1) { - currentPage--; - renderPosts(); - } - }); - - nextBtn.addEventListener('click', () => { - const totalPages = Math.ceil(filteredPosts.length / postsPerPage); - if (currentPage < totalPages) { - currentPage++; - renderPosts(); - } - }); - - // Pagination - page numbers (event delegation) - const pageNumbersEl = document.getElementById('page-numbers'); - pageNumbersEl.addEventListener('click', e => { - const pageBtn = e.target.closest('.page-number'); - if (pageBtn) { - currentPage = parseInt(pageBtn.dataset.page, 10); - renderPosts(); - } - }); - - // Remove filter tags (event delegation) - const filterTagsEl = document.getElementById('filter-tags'); - filterTagsEl.addEventListener('click', e => { - const removeBtn = e.target.closest('[data-remove-filter]'); - if (removeBtn) { - const filterType = removeBtn.dataset.removeFilter; - removeFilter(filterType); - } - }); -} - -/** - * Show error message - */ -function showError(message) { - const gridEl = document.getElementById('blog-grid'); - gridEl.innerHTML = ` -
-
- - - -
-

Error

-

${escapeHtml(message)}

-
-
-
- `; -} - -/** - * Escape HTML to prevent XSS - */ -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -/** - * Newsletter Modal Functionality - */ -function setupNewsletterModal() { - const modal = document.getElementById('newsletter-modal'); - const openBtn = document.getElementById('open-newsletter-modal'); - const closeBtn = document.getElementById('close-newsletter-modal'); - const cancelBtn = document.getElementById('cancel-newsletter'); - const form = document.getElementById('newsletter-form'); - const successMsg = document.getElementById('newsletter-success'); - const errorMsg = document.getElementById('newsletter-error'); - const errorText = document.getElementById('newsletter-error-message'); - const submitBtn = document.getElementById('newsletter-submit'); - - // Open modal - if (openBtn) { - openBtn.addEventListener('click', () => { - modal.classList.remove('hidden'); - document.getElementById('newsletter-email').focus(); - }); - } - - // Close modal - function closeModal() { - modal.classList.add('hidden'); - form.reset(); - successMsg.classList.add('hidden'); - errorMsg.classList.add('hidden'); - } - - if (closeBtn) { - closeBtn.addEventListener('click', closeModal); - } - - if (cancelBtn) { - cancelBtn.addEventListener('click', closeModal); - } - - // Close on backdrop click - modal.addEventListener('click', (e) => { - if (e.target === modal) { - closeModal(); - } - }); - - // Close on Escape key - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && !modal.classList.contains('hidden')) { - closeModal(); - } - }); - - // Handle form submission - if (form) { - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - // Reset messages - successMsg.classList.add('hidden'); - errorMsg.classList.add('hidden'); - - const email = document.getElementById('newsletter-email').value; - const name = document.getElementById('newsletter-name').value; - - // Disable submit button - submitBtn.disabled = true; - submitBtn.textContent = 'Subscribing...'; - - try { - const response = await fetch('/api/newsletter/subscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - email, - name: name || null, - source: 'blog' - }) - }); - - const data = await response.json(); - - if (response.ok && data.success) { - // Show success message - successMsg.classList.remove('hidden'); - form.reset(); - - // Close modal after 2 seconds - setTimeout(() => { - closeModal(); - }, 2000); - } else { - // Show error message - errorText.textContent = data.error || 'Failed to subscribe. Please try again.'; - errorMsg.classList.remove('hidden'); - } - } catch (error) { - console.error('Newsletter subscription error:', error); - errorText.textContent = 'Network error. Please check your connection and try again.'; - errorMsg.classList.remove('hidden'); - } finally { - // Re-enable submit button - submitBtn.disabled = false; - submitBtn.textContent = 'Subscribe'; - } - }); - } -} - -// Initialize on page load -document.addEventListener('DOMContentLoaded', () => { - init(); - setupNewsletterModal(); -}); diff --git a/public/js/case-submission.js b/public/js/case-submission.js deleted file mode 100644 index b178b188..00000000 --- a/public/js/case-submission.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Case Submission Form Handler - */ - -const form = document.getElementById('case-submission-form'); -const submitButton = document.getElementById('submit-button'); -const successMessage = document.getElementById('success-message'); -const errorMessage = document.getElementById('error-message'); - -form.addEventListener('submit', async (e) => { - e.preventDefault(); - - // Hide previous messages - successMessage.style.display = 'none'; - errorMessage.style.display = 'none'; - - // Disable submit button - submitButton.disabled = true; - submitButton.textContent = 'Submitting...'; - - // Collect form data - const evidenceText = document.getElementById('case-evidence').value; - const evidence = evidenceText - ? evidenceText.split('\n').filter(line => line.trim()) - : []; - - const formData = { - submitter: { - name: document.getElementById('submitter-name').value, - email: document.getElementById('submitter-email').value, - organization: document.getElementById('submitter-organization').value || null, - public: document.getElementById('submitter-public').checked - }, - case_study: { - title: document.getElementById('case-title').value, - description: document.getElementById('case-description').value, - failure_mode: document.getElementById('case-failure-mode').value, - tractatus_applicability: document.getElementById('case-tractatus').value, - evidence: evidence, - attachments: [] - } - }; - - try { - const response = await fetch('/api/cases/submit', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - - const data = await response.json(); - - if (response.ok) { - // Success - successMessage.textContent = data.message || 'Thank you for your submission. We will review it shortly.'; - successMessage.style.display = 'block'; - form.reset(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } else { - // Error - errorMessage.textContent = data.message || 'An error occurred. Please try again.'; - errorMessage.style.display = 'block'; - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - - } catch (error) { - console.error('Submit error:', error); - errorMessage.textContent = 'Network error. Please check your connection and try again.'; - errorMessage.style.display = 'block'; - window.scrollTo({ top: 0, behavior: 'smooth' }); - } finally { - // Re-enable submit button - submitButton.disabled = false; - submitButton.textContent = 'Submit Case Study'; - } -}); diff --git a/public/js/check-version.js b/public/js/check-version.js deleted file mode 100644 index bdca3b61..00000000 --- a/public/js/check-version.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Version Check Script - * Tests if browser is using cached JavaScript files - */ - -// Get the version from the main docs page -fetch('/docs.html?' + Date.now()) - .then(r => r.text()) - .then(html => { - const match = html.match(/docs-app\.js\?v=(\d+)/); - const version = match ? match[1] : 'NOT FOUND'; - - const expected = '1759828916'; - const correct = version === expected; - - // Now fetch the actual JavaScript - return fetch('/js/docs-app.js?v=' + version + '&' + Date.now()) - .then(r => r.text()) - .then(js => { - const hasNewHandler = js.includes('window.location.href='); - const hasOldHandler = js.includes('event.stopPropagation()'); - - let html = ''; - - if (correct && hasNewHandler) { - html = ` -
-

✅ Version is CORRECT

-

JavaScript version: ${version}

-

Handler includes: window.location.href

-

Downloads should work now!

-
- `; - } else { - html = ` -
-

❌ Version is WRONG

-

JavaScript version loaded: ${version}

-

Expected: ${expected}

-

Has new handler: ${hasNewHandler ? '✅ YES' : '❌ NO'}

-

Your browser is serving cached files!

-
-
-

Cached JavaScript Snippet:

-
${js.substring(js.indexOf('onclick='), js.indexOf('onclick=') + 200).replace(//g, '>')}
-
- `; - } - - document.getElementById('results').innerHTML = html; - }); - }) - .catch(err => { - document.getElementById('results').innerHTML = ` -
-

Error

-

${err.message}

-
- `; - }); diff --git a/public/js/components/activity-timeline.js b/public/js/components/activity-timeline.js deleted file mode 100644 index d3d57ecd..00000000 --- a/public/js/components/activity-timeline.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Framework Activity Timeline - * Tractatus Framework - Phase 3: Data Visualization - * - * Visual timeline showing framework component interactions - * Color-coded by service - */ - -class ActivityTimeline { - constructor(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - console.error(`[ActivityTimeline] Container #${containerId} not found`); - return; - } - - this.currentPath = 'fast'; // Default to fast path - - // Define three execution paths with realistic timings - this.pathProfiles = { - fast: { - name: 'Fast Path', - description: 'Simple request, all checks pass', - totalTime: '65ms', - events: [ - { time: '0ms', timeMs: 0, service: 'instruction', name: 'InstructionPersistence', action: 'Load cached instructions', color: '#4338ca' }, - { time: '5ms', timeMs: 5, service: 'validator', name: 'CrossReferenceValidator', action: 'Quick validation check', color: '#6d28d9' }, - { time: '15ms', timeMs: 15, service: 'boundary', name: 'BoundaryEnforcer', action: 'Auto-approved operation', color: '#047857' }, - { time: '25ms', timeMs: 25, service: 'pressure', name: 'ContextPressureMonitor', action: 'Normal pressure detected', color: '#b45309' }, - { time: '50ms', timeMs: 50, service: 'validator', name: 'CrossReferenceValidator', action: 'Final validation', color: '#6d28d9' }, - { time: '65ms', timeMs: 65, service: 'pressure', name: 'ContextPressureMonitor', action: 'Update metrics', color: '#b45309' } - ] - }, - standard: { - name: 'Standard Path', - description: 'Needs validation and verification', - totalTime: '135ms', - events: [ - { time: '0ms', timeMs: 0, service: 'instruction', name: 'InstructionPersistence', action: 'Load HIGH persistence instructions', color: '#4338ca' }, - { time: '8ms', timeMs: 8, service: 'validator', name: 'CrossReferenceValidator', action: 'Verify against instruction history', color: '#6d28d9' }, - { time: '30ms', timeMs: 30, service: 'boundary', name: 'BoundaryEnforcer', action: 'Check approval requirements', color: '#047857' }, - { time: '55ms', timeMs: 55, service: 'pressure', name: 'ContextPressureMonitor', action: 'Calculate pressure level', color: '#b45309' }, - { time: '95ms', timeMs: 95, service: 'metacognitive', name: 'MetacognitiveVerifier', action: 'Verify operation alignment', color: '#be185d' }, - { time: '120ms', timeMs: 120, service: 'validator', name: 'CrossReferenceValidator', action: 'Final validation check', color: '#6d28d9' }, - { time: '135ms', timeMs: 135, service: 'pressure', name: 'ContextPressureMonitor', action: 'Update pressure metrics', color: '#b45309' } - ] - }, - complex: { - name: 'Complex Path', - description: 'Requires deliberation and consensus', - totalTime: '285ms', - events: [ - { time: '0ms', timeMs: 0, service: 'instruction', name: 'InstructionPersistence', action: 'Load HIGH persistence instructions', color: '#4338ca' }, - { time: '8ms', timeMs: 8, service: 'validator', name: 'CrossReferenceValidator', action: 'Verify request against instruction history', color: '#6d28d9' }, - { time: '35ms', timeMs: 35, service: 'boundary', name: 'BoundaryEnforcer', action: 'Check if request requires human approval', color: '#047857' }, - { time: '60ms', timeMs: 60, service: 'pressure', name: 'ContextPressureMonitor', action: 'Calculate current pressure level', color: '#b45309' }, - { time: '105ms', timeMs: 105, service: 'metacognitive', name: 'MetacognitiveVerifier', action: 'Verify operation alignment', color: '#be185d' }, - { time: '160ms', timeMs: 160, service: 'deliberation', name: 'PluralisticDeliberation', action: 'Coordinate stakeholder perspectives', color: '#0f766e' }, - { time: '255ms', timeMs: 255, service: 'validator', name: 'CrossReferenceValidator', action: 'Final validation check', color: '#6d28d9' }, - { time: '285ms', timeMs: 285, service: 'pressure', name: 'ContextPressureMonitor', action: 'Update pressure metrics', color: '#b45309' } - ] - } - }; - - // Initialize with fast path by default - this.events = this.pathProfiles[this.currentPath].events; - - this.init(); - } - - init() { - this.render(); - this.isSimulating = false; - console.log('[ActivityTimeline] Initialized'); - } - - render() { - const eventsHTML = this.events.map((event, index) => ` -
-
- ${event.time} -
-
-
-
-
-
- ${event.name} -
-
${event.action}
-
-
- `).join(''); - - const currentProfile = this.pathProfiles[this.currentPath]; - - this.container.innerHTML = ` -
-
-

Governance Flow

-

Estimated timing based on current performance data

-
- - -
-
Execution Path:
-
- - - -
-
${currentProfile.description}
-
- - -
-

- This shows the framework's governance components working together to validate and process each request. Each component has a specific role in ensuring safe, values-aligned AI operation. -

-

- Note: Timing values are estimates based on current performance statistics and may vary in production. -

-
- -
- ${eventsHTML} -
- -
- Total processing time: ${currentProfile.totalTime} | All services coordinated -
-
- `; - - // Apply colors via JavaScript (CSP-compliant) - this.applyColors(); - - // Attach event listeners to path radio buttons - this.attachPathListeners(); - } - - attachPathListeners() { - const radios = this.container.querySelectorAll('.path-radio'); - radios.forEach(radio => { - radio.addEventListener('change', (e) => { - this.setPath(e.target.value); - }); - }); - } - - setPath(pathName) { - if (!this.pathProfiles[pathName]) { - console.error(`[ActivityTimeline] Unknown path: ${pathName}`); - return; - } - - console.log(`[ActivityTimeline] Switching to ${pathName} path`); - this.currentPath = pathName; - this.events = this.pathProfiles[pathName].events; - this.render(); - } - - applyColors() { - document.querySelectorAll('.service-dot').forEach(dot => { - const color = dot.getAttribute('data-color'); - dot.style.backgroundColor = color; - }); - - document.querySelectorAll('.service-name').forEach(name => { - const color = name.getAttribute('data-color'); - name.style.color = color; - }); - } - - activateEvent(index) { - const eventElement = this.container.querySelector(`[data-event-index="${index}"]`); - if (!eventElement) return; - - const event = this.events[index]; - - // Highlight the event card - eventElement.style.borderColor = event.color; - eventElement.style.backgroundColor = `${event.color}10`; // 10% opacity - eventElement.style.boxShadow = `0 4px 12px ${event.color}40`; - - // Enlarge and pulse the service dot - const dot = eventElement.querySelector('.service-dot'); - if (dot) { - dot.style.width = '12px'; - dot.style.height = '12px'; - dot.style.boxShadow = `0 0 8px ${event.color}`; - } - - console.log(`[ActivityTimeline] Activated event ${index}: ${event.name}`); - } - - deactivateEvent(index) { - const eventElement = this.container.querySelector(`[data-event-index="${index}"]`); - if (!eventElement) return; - - // Reset to default styling - eventElement.style.borderColor = '#e5e7eb'; - eventElement.style.backgroundColor = '#ffffff'; - eventElement.style.boxShadow = ''; - - // Reset service dot - const dot = eventElement.querySelector('.service-dot'); - if (dot) { - dot.style.width = '12px'; - dot.style.height = '12px'; - dot.style.boxShadow = ''; - } - } - - async simulateFlow() { - if (this.isSimulating) { - console.log('[ActivityTimeline] Already simulating, ignoring request'); - return; - } - - this.isSimulating = true; - console.log('[ActivityTimeline] Starting governance flow simulation'); - - // Reset all events first - for (let i = 0; i < this.events.length; i++) { - this.deactivateEvent(i); - } - - // Simulate each event activation with realistic timing - for (let i = 0; i < this.events.length; i++) { - const event = this.events[i]; - const prevEvent = i > 0 ? this.events[i - 1] : null; - - // Calculate actual delay based on event timing (scaled 2x for visibility) - const delay = prevEvent ? (event.timeMs - prevEvent.timeMs) * 2 : 0; - - await new Promise(resolve => setTimeout(resolve, delay)); - - // Deactivate previous event - if (i > 0) { - this.deactivateEvent(i - 1); - } - - // Activate current event - this.activateEvent(i); - console.log(`[ActivityTimeline] Event ${i} activated at ${event.time} (delay: ${delay}ms)`); - } - - // Keep the last event active for a moment, then deactivate - await new Promise(resolve => setTimeout(resolve, 800)); - this.deactivateEvent(this.events.length - 1); - - this.isSimulating = false; - console.log('[ActivityTimeline] Governance flow simulation complete'); - } - - reset() { - console.log('[ActivityTimeline] Resetting timeline'); - for (let i = 0; i < this.events.length; i++) { - this.deactivateEvent(i); - } - this.isSimulating = false; - } -} - -// Auto-initialize if container exists -if (typeof window !== 'undefined') { - function initActivityTimeline() { - console.log('[ActivityTimeline] Attempting to initialize, readyState:', document.readyState); - const container = document.getElementById('activity-timeline'); - if (container) { - console.log('[ActivityTimeline] Container found, creating instance'); - window.activityTimeline = new ActivityTimeline('activity-timeline'); - } else { - console.error('[ActivityTimeline] Container #activity-timeline not found in DOM'); - } - } - - // Initialize immediately if DOM is already loaded, otherwise wait for DOMContentLoaded - console.log('[ActivityTimeline] Script loaded, readyState:', document.readyState); - if (document.readyState === 'loading') { - console.log('[ActivityTimeline] Waiting for DOMContentLoaded'); - document.addEventListener('DOMContentLoaded', initActivityTimeline); - } else { - console.log('[ActivityTimeline] DOM already loaded, initializing immediately'); - initActivityTimeline(); - } -} - -// Export for module systems -if (typeof module !== 'undefined' && module.exports) { - module.exports = ActivityTimeline; -} diff --git a/public/js/components/code-copy-button.js b/public/js/components/code-copy-button.js deleted file mode 100644 index 07bf5f2c..00000000 --- a/public/js/components/code-copy-button.js +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Code Copy Button Component - * Tractatus Framework - Phase 3: Interactive Documentation - * - * Adds "Copy" buttons to all code blocks for easy copying - * Shows success feedback on copy - */ - -class CodeCopyButtons { - constructor() { - this.buttonClass = 'code-copy-btn'; - this.successClass = 'code-copy-success'; - this.init(); - } - - init() { - // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => this.addCopyButtons()); - } else { - this.addCopyButtons(); - } - - console.log('[CodeCopyButtons] Initialized'); - } - - addCopyButtons() { - // Find all code blocks (pre elements) - const codeBlocks = document.querySelectorAll('pre'); - console.log(`[CodeCopyButtons] Found ${codeBlocks.length} code blocks`); - - codeBlocks.forEach((pre, index) => { - // Skip if already has a copy button - if (pre.querySelector(`.${this.buttonClass}`)) { - return; - } - - // Make pre relative positioned for absolute button - pre.style.position = 'relative'; - - // Create copy button - const button = this.createCopyButton(pre, index); - - // Add button to pre element - pre.appendChild(button); - }); - } - - createCopyButton(pre, index) { - const button = document.createElement('button'); - button.className = `${this.buttonClass} absolute top-2 right-2 px-3 py-1 text-xs font-medium rounded transition-all duration-200`; - button.style.cssText = ` - background: rgba(255, 255, 255, 0.1); - color: #e5e7eb; - border: 1px solid rgba(255, 255, 255, 0.2); - `; - button.textContent = 'Copy'; - button.setAttribute('aria-label', 'Copy code to clipboard'); - button.setAttribute('data-code-index', index); - - // Add hover styles via class - button.addEventListener('mouseenter', () => { - button.style.background = 'rgba(255, 255, 255, 0.2)'; - }); - - button.addEventListener('mouseleave', () => { - if (!button.classList.contains(this.successClass)) { - button.style.background = 'rgba(255, 255, 255, 0.1)'; - } - }); - - // Add click handler - button.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - this.copyCode(pre, button); - }); - - return button; - } - - async copyCode(pre, button) { - // Get code content (find code element inside pre) - const codeElement = pre.querySelector('code'); - const code = codeElement ? codeElement.textContent : pre.textContent; - - try { - // Use Clipboard API - await navigator.clipboard.writeText(code); - - // Show success feedback - this.showSuccess(button); - - console.log('[CodeCopyButtons] Code copied to clipboard'); - } catch (err) { - console.error('[CodeCopyButtons] Failed to copy code:', err); - - // Fallback: try using execCommand - this.fallbackCopy(code, button); - } - } - - fallbackCopy(text, button) { - // Create temporary textarea - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - - try { - textarea.select(); - const successful = document.execCommand('copy'); - - if (successful) { - this.showSuccess(button); - console.log('[CodeCopyButtons] Code copied using fallback method'); - } else { - this.showError(button); - } - } catch (err) { - console.error('[CodeCopyButtons] Fallback copy failed:', err); - this.showError(button); - } finally { - document.body.removeChild(textarea); - } - } - - showSuccess(button) { - button.classList.add(this.successClass); - button.textContent = '✓ Copied!'; - button.style.background = 'rgba(16, 185, 129, 0.3)'; // Green - button.style.borderColor = 'rgba(16, 185, 129, 0.5)'; - button.style.color = '#d1fae5'; - - // Reset after 2 seconds - setTimeout(() => { - button.classList.remove(this.successClass); - button.textContent = 'Copy'; - button.style.background = 'rgba(255, 255, 255, 0.1)'; - button.style.borderColor = 'rgba(255, 255, 255, 0.2)'; - button.style.color = '#e5e7eb'; - }, 2000); - } - - showError(button) { - button.textContent = '✗ Failed'; - button.style.background = 'rgba(239, 68, 68, 0.3)'; // Red - button.style.borderColor = 'rgba(239, 68, 68, 0.5)'; - - // Reset after 2 seconds - setTimeout(() => { - button.textContent = 'Copy'; - button.style.background = 'rgba(255, 255, 255, 0.1)'; - button.style.borderColor = 'rgba(255, 255, 255, 0.2)'; - }, 2000); - } - - // Public method to refresh buttons (useful for dynamically loaded content) - refresh() { - this.addCopyButtons(); - } -} - -// Auto-initialize when script loads -if (typeof window !== 'undefined') { - window.codeCopyButtons = new CodeCopyButtons(); - - // Listen for custom event from document viewer for dynamic content - document.addEventListener('documentLoaded', () => { - console.log('[CodeCopyButtons] Document loaded, refreshing buttons'); - window.codeCopyButtons.refresh(); - }); -} - -// Export for module usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = CodeCopyButtons; -} diff --git a/public/js/components/coming-soon-overlay.js b/public/js/components/coming-soon-overlay.js deleted file mode 100644 index 7372fcc5..00000000 --- a/public/js/components/coming-soon-overlay.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Coming Soon Overlay - * Displays over Koha pages until Stripe is configured - */ - -(function() { - 'use strict'; - - // Check if we should show the overlay - const shouldShowOverlay = () => { - // Only show on Koha pages - const isKohaPage = window.location.pathname.includes('/koha'); - return isKohaPage; - }; - - // Create and inject overlay - if (shouldShowOverlay()) { - const overlayHTML = ` -
-
-

- Koha Donation System -

-

- Coming Soon -

-
-

- What is Koha? -

-

- Koha (Māori for "gift") is our upcoming donation system to support the Tractatus Framework. - We're currently finalizing payment processing integration and will launch soon. -

-
-

- Infrastructure deployed and ready. Payment processing activation in progress. -

- - Return to Homepage - - -
-
- `; - - document.body.insertAdjacentHTML('beforeend', overlayHTML); - } -})(); diff --git a/public/js/components/currency-selector.js b/public/js/components/currency-selector.js deleted file mode 100644 index b5a897b9..00000000 --- a/public/js/components/currency-selector.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Currency Selector Component - * Dropdown for selecting donation currency - */ - -(function() { - 'use strict'; - - // Currency selector HTML - const selectorHTML = ` -
- - -

- Prices are automatically converted from NZD. Your selection is saved for future visits. -

-
- `; - - // Initialize currency selector - function initCurrencySelector() { - // Find container (should have id="currency-selector-container") - const container = document.getElementById('currency-selector-container'); - if (!container) { - console.warn('Currency selector container not found'); - return; - } - - // Insert selector HTML - container.innerHTML = selectorHTML; - - // Get select element - const select = document.getElementById('currency-select'); - - // Set initial value from detected currency - const detectedCurrency = detectUserCurrency(); - select.value = detectedCurrency; - - // Trigger initial price update - if (typeof window.updatePricesForCurrency === 'function') { - window.updatePricesForCurrency(detectedCurrency); - } - - // Listen for changes - select.addEventListener('change', function(e) { - const newCurrency = e.target.value; - - // Save preference - saveCurrencyPreference(newCurrency); - - // Update prices - if (typeof window.updatePricesForCurrency === 'function') { - window.updatePricesForCurrency(newCurrency); - } - }); - } - - // Auto-initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initCurrencySelector); - } else { - initCurrencySelector(); - } - - // Expose init function globally - window.initCurrencySelector = initCurrencySelector; - -})(); diff --git a/public/js/components/document-cards.js b/public/js/components/document-cards.js deleted file mode 100644 index cbf6b79a..00000000 --- a/public/js/components/document-cards.js +++ /dev/null @@ -1,422 +0,0 @@ -/** - * Document Cards Component - * Renders document sections as interactive cards - */ - -class DocumentCards { - constructor(containerId) { - this.container = document.getElementById(containerId); - this.currentDocument = null; - this.modalViewer = new ModalViewer(); - } - - /** - * Render document as card grid - */ - render(document) { - if (!document || !document.sections || document.sections.length === 0) { - this.renderTraditionalView(document); - return; - } - - this.currentDocument = document; - - // Create document header - const headerHtml = this.renderHeader(document); - - // Group sections by category - const sectionsByCategory = this.groupByCategory(document.sections); - - // Render card grid - const cardsHtml = this.renderCardGrid(sectionsByCategory); - - this.container.innerHTML = ` - ${headerHtml} - ${cardsHtml} - `; - - // Add event listeners after a brief delay to ensure DOM is ready - setTimeout(() => { - this.attachEventListeners(); - }, 0); - } - - /** - * Render document header - */ - renderHeader(document) { - const version = document.metadata?.version || ''; - const dateUpdated = document.metadata?.date_updated - ? new Date(document.metadata.date_updated).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short' - }) - : ''; - - const versionText = version ? `v${version}` : ''; - const metaText = [versionText, dateUpdated ? `Updated ${dateUpdated}` : ''] - .filter(Boolean) - .join(' | '); - - const hasToC = document.toc && document.toc.length > 0; - - return ` -
-
-

${document.title}

- ${metaText ? `

${metaText}

` : ''} - ${document.sections ? `

${document.sections.length} sections

` : ''} -
-
- ${hasToC ? ` - - ` : ''} - - - - - -
-
- `; - } - - /** - * Group sections by category - */ - groupByCategory(sections) { - const groups = { - conceptual: [], - practical: [], - technical: [], - reference: [], - critical: [] - }; - - sections.forEach(section => { - const category = section.category || 'conceptual'; - if (groups[category]) { - groups[category].push(section); - } else { - groups.conceptual.push(section); - } - }); - - return groups; - } - - /** - * Render card grid - */ - renderCardGrid(sectionsByCategory) { - const categoryConfig = { - conceptual: { icon: '📘', label: 'Conceptual', color: 'blue' }, - practical: { icon: '✨', label: 'Practical', color: 'green' }, - technical: { icon: '🔧', label: 'Technical', color: 'purple' }, - reference: { icon: '📋', label: 'Reference', color: 'gray' }, - critical: { icon: '⚠️', label: 'Critical', color: 'amber' } - }; - - let html = '
'; - - // Render each category that has sections - for (const [category, sections] of Object.entries(sectionsByCategory)) { - if (sections.length === 0) continue; - - const config = categoryConfig[category]; - - html += ` -
-

- ${config.icon} - ${config.label} -

-
- ${sections.map(section => this.renderCard(section, config.color)).join('')} -
-
- `; - } - - html += '
'; - - return html; - } - - /** - * Render individual card - */ - renderCard(section, color) { - const levelIcons = { - basic: '○', - intermediate: '◐', - advanced: '●' - }; - - const levelIcon = levelIcons[section.technicalLevel] || '○'; - const levelLabel = section.technicalLevel.charAt(0).toUpperCase() + section.technicalLevel.slice(1); - - const borderColor = { - blue: 'border-blue-400', - green: 'border-green-400', - purple: 'border-purple-400', - gray: 'border-gray-400', - amber: 'border-amber-400' - }[color] || 'border-blue-400'; - - const hoverColor = { - blue: 'hover:border-blue-600 hover:shadow-blue-100', - green: 'hover:border-green-600 hover:shadow-green-100', - purple: 'hover:border-purple-600 hover:shadow-purple-100', - gray: 'hover:border-gray-600 hover:shadow-gray-100', - amber: 'hover:border-amber-600 hover:shadow-amber-100' - }[color] || 'hover:border-blue-600'; - - const bgColor = { - blue: 'bg-blue-50', - green: 'bg-green-50', - purple: 'bg-purple-50', - gray: 'bg-gray-50', - amber: 'bg-amber-50' - }[color] || 'bg-blue-50'; - - return ` -
-

${section.title}

-

${section.excerpt}

-
- ${section.readingTime} min read - ${levelIcon} ${levelLabel} -
-
- `; - } - - /** - * Fallback: render traditional view for documents without sections - */ - renderTraditionalView(document) { - if (!document) return; - - this.container.innerHTML = ` -
- ${document.content_html} -
- `; - } - - /** - * Attach event listeners to cards - */ - attachEventListeners() { - const cards = this.container.querySelectorAll('.doc-card'); - - cards.forEach(card => { - card.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - const sectionSlug = card.dataset.sectionSlug; - const section = this.currentDocument.sections.find(s => s.slug === sectionSlug); - - if (section) { - this.modalViewer.show(section, this.currentDocument.sections); - } - }); - }); - - // Attach ToC button listener - const tocButton = document.getElementById('toc-button'); - if (tocButton) { - tocButton.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - if (typeof openToCModal === 'function') { - openToCModal(); - } - }); - } - } -} - -/** - * Modal Viewer Component - * Displays section content in a modal - */ -class ModalViewer { - constructor() { - this.modal = null; - this.currentSection = null; - this.allSections = []; - this.currentIndex = 0; - this.createModal(); - } - - /** - * Create modal structure - */ - createModal() { - const modalHtml = ` -
-
- -
- - -
- - - - - -
- - - -
-
-
- `; - - document.body.insertAdjacentHTML('beforeend', modalHtml); - - this.modal = document.getElementById('section-modal'); - this.attachModalListeners(); - } - - /** - * Show modal with section content - */ - show(section, allSections) { - this.currentSection = section; - this.allSections = allSections; - this.currentIndex = allSections.findIndex(s => s.slug === section.slug); - - // Update content - const titleEl = document.getElementById('modal-title'); - const contentEl = document.getElementById('modal-content'); - - if (!titleEl || !contentEl) { - return; - } - - titleEl.textContent = section.title; - - // Remove duplicate title (H1 or H2) from content (it's already in modal header) - let contentHtml = section.content_html; - - // Try removing h1 first, then h2 - const firstH1Match = contentHtml.match(/]*>.*?<\/h1>/); - if (firstH1Match) { - contentHtml = contentHtml.replace(firstH1Match[0], ''); - } else { - const firstH2Match = contentHtml.match(/]*>.*?<\/h2>/); - if (firstH2Match) { - contentHtml = contentHtml.replace(firstH2Match[0], ''); - } - } - - contentEl.innerHTML = contentHtml; - - // Update navigation - this.updateNavigation(); - - // Show modal - this.modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; - - // Scroll to top of content - contentEl.scrollTop = 0; - } - - /** - * Hide modal - */ - hide() { - this.modal.style.display = 'none'; - document.body.style.overflow = ''; - } - - /** - * Update navigation buttons - */ - updateNavigation() { - const prevBtn = document.getElementById('modal-prev'); - const nextBtn = document.getElementById('modal-next'); - const progress = document.getElementById('modal-progress'); - - prevBtn.disabled = this.currentIndex === 0; - nextBtn.disabled = this.currentIndex === this.allSections.length - 1; - - progress.textContent = `${this.currentIndex + 1} of ${this.allSections.length}`; - } - - /** - * Navigate to previous section - */ - showPrevious() { - if (this.currentIndex > 0) { - this.show(this.allSections[this.currentIndex - 1], this.allSections); - } - } - - /** - * Navigate to next section - */ - showNext() { - if (this.currentIndex < this.allSections.length - 1) { - this.show(this.allSections[this.currentIndex + 1], this.allSections); - } - } - - /** - * Attach modal event listeners - */ - attachModalListeners() { - // Close button - document.getElementById('modal-close').addEventListener('click', () => this.hide()); - - // Navigation buttons - document.getElementById('modal-prev').addEventListener('click', () => this.showPrevious()); - document.getElementById('modal-next').addEventListener('click', () => this.showNext()); - - // Close on background click - this.modal.addEventListener('click', (e) => { - if (e.target === this.modal) { - this.hide(); - } - }); - - // Keyboard navigation - document.addEventListener('keydown', (e) => { - // Check if modal is visible using display style instead of hidden class - if (this.modal.style.display === 'flex') { - if (e.key === 'Escape') { - this.hide(); - } else if (e.key === 'ArrowLeft') { - this.showPrevious(); - } else if (e.key === 'ArrowRight') { - this.showNext(); - } - } - }); - } -} diff --git a/public/js/components/document-viewer.js b/public/js/components/document-viewer.js deleted file mode 100644 index 8bbdbaab..00000000 --- a/public/js/components/document-viewer.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Document Viewer Component - * Displays framework documentation with TOC and navigation - */ - -class DocumentViewer { - constructor(containerId = 'document-viewer') { - this.container = document.getElementById(containerId); - this.currentDocument = null; - } - - /** - * Render document - */ - async render(documentSlug) { - if (!this.container) { - console.error('Document viewer container not found'); - return; - } - - try { - // Show loading state - this.showLoading(); - - // Fetch document - const response = await API.Documents.get(documentSlug); - - if (!response.success) { - throw new Error('Document not found'); - } - - this.currentDocument = response.document; - this.showDocument(); - - } catch (error) { - this.showError(error.message); - } - } - - /** - * Show loading state - */ - showLoading() { - this.container.innerHTML = ` -
-
-
-

Loading document...

-
-
- `; - } - - /** - * Show document content - */ - showDocument() { - const doc = this.currentDocument; - - this.container.innerHTML = ` -
- -
- ${doc.quadrant ? ` - - ${doc.quadrant} - - ` : ''} -

${this.escapeHtml(doc.title)}

- ${doc.metadata?.version ? ` -

Version ${doc.metadata.version}

- ` : ''} -
- - - ${doc.toc && doc.toc.length > 0 ? this.renderTOC(doc.toc) : ''} - - -
- ${doc.content_html} -
- - -
-
- ${doc.created_at ? `

Created: ${new Date(doc.created_at).toLocaleDateString()}

` : ''} - ${doc.updated_at ? `

Updated: ${new Date(doc.updated_at).toLocaleDateString()}

` : ''} -
-
-
- `; - - // Add smooth scroll to TOC links - this.initializeTOCLinks(); - } - - /** - * Render table of contents - */ - renderTOC(toc) { - return ` -
-

Table of Contents

- -
- `; - } - - /** - * Initialize TOC links for smooth scrolling - */ - initializeTOCLinks() { - this.container.querySelectorAll('a[href^="#"]').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const id = link.getAttribute('href').slice(1); - const target = document.getElementById(id); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }); - }); - } - - /** - * Show error state - */ - showError(message) { - this.container.innerHTML = ` -
-
- - - -
-

Document Not Found

-

${this.escapeHtml(message)}

- - ← Browse all documents - -
- `; - } - - /** - * Escape HTML to prevent XSS - */ - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } -} - -// Export as global -window.DocumentViewer = DocumentViewer; diff --git a/public/js/components/footer.js b/public/js/components/footer.js deleted file mode 100644 index 788684f5..00000000 --- a/public/js/components/footer.js +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Footer Component - i18n-enabled - * Shared footer for all Tractatus pages with language persistence - */ - -(function() { - 'use strict'; - - class TractatusFooter { - constructor() { - this.init(); - } - - init() { - // Wait for I18n to be ready before rendering - if (window.I18n && window.I18n.translations && Object.keys(window.I18n.translations).length > 0) { - this.render(); - this.attachEventListeners(); - } else { - // If I18n not ready, wait for it - const checkI18n = setInterval(() => { - if (window.I18n && window.I18n.translations && Object.keys(window.I18n.translations).length > 0) { - clearInterval(checkI18n); - this.render(); - this.attachEventListeners(); - } - }, 100); - - // Fallback timeout - render without i18n after 2 seconds - setTimeout(() => { - clearInterval(checkI18n); - if (!document.querySelector('footer[role="contentinfo"]')) { - this.render(); - this.attachEventListeners(); - } - }, 2000); - } - } - - render() { - const currentYear = new Date().getFullYear(); - - // Create footer HTML with data-i18n attributes - const footerHTML = ` -
-
- - -
- - -
-

Tractatus Framework

-

- Architectural constraints for AI safety that preserve human agency through structural, not aspirational, enforcement. -

-
- - -
-

Documentation

- -
- - - - - -
-

Legal

- -
- -
- - -
- - -
-

- Te Tiriti o Waitangi: - We acknowledge Te Tiriti o Waitangi and our commitment to partnership, protection, and participation. This project respects Māori data sovereignty (rangatiratanga) and collective guardianship (kaitiakitanga). -

-
- - -
-

- © ${currentYear} John G Stroh. Licensed under Apache 2.0. -

-

- Made in Aotearoa New Zealand 🇳🇿 -

-
- -
- -
-
- `; - - // Insert footer at end of body - const existingFooter = document.querySelector('footer[role="contentinfo"]'); - if (existingFooter) { - existingFooter.outerHTML = footerHTML; - } else if (document.body) { - document.body.insertAdjacentHTML('beforeend', footerHTML); - } else { - // If body not ready, wait for DOM - document.addEventListener('DOMContentLoaded', () => { - document.body.insertAdjacentHTML('beforeend', footerHTML); - }); - } - - // Apply translations if I18n is available - if (window.I18n && window.I18n.applyTranslations) { - window.I18n.applyTranslations(); - } - } - - attachEventListeners() { - // Listen for language changes and re-render footer - window.addEventListener('languageChanged', (event) => { - console.log('[Footer] Language changed to:', event.detail.language); - this.render(); - }); - } - } - - // Auto-initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => new TractatusFooter()); - } else { - new TractatusFooter(); - } - -})(); diff --git a/public/js/components/language-selector.js b/public/js/components/language-selector.js deleted file mode 100644 index 434426df..00000000 --- a/public/js/components/language-selector.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Language Selector Component - * Simple icon-based selector for all devices - */ - -(function() { - const supportedLanguages = [ - { code: 'en', name: 'English', flag: '🇬🇧' }, - { code: 'de', name: 'Deutsch', flag: '🇩🇪' }, - { code: 'fr', name: 'Français', flag: '🇫🇷' }, - { code: 'mi', name: 'Te Reo Māori', flag: '🇳🇿', disabled: true, tooltip: 'Planned' } - ]; - - function createLanguageSelector() { - const container = document.getElementById('language-selector-container'); - if (!container) return; - - const currentLang = (window.I18n && window.I18n.currentLang) || 'en'; - - const selectorHTML = ` - -
- ${supportedLanguages.map(lang => ` - - `).join('')} -
- `; - - container.innerHTML = selectorHTML; - - // Add event listeners - attachEventListeners(currentLang); - } - - function attachEventListeners(currentLang) { - // Icon buttons - const iconButtons = document.querySelectorAll('.language-icon-btn:not([disabled])'); - iconButtons.forEach(button => { - button.addEventListener('click', function() { - const selectedLang = this.getAttribute('data-lang'); - if (window.I18n) { - window.I18n.setLanguage(selectedLang); - } - - // Update active state - document.querySelectorAll('.language-icon-btn').forEach(btn => { - if (!btn.disabled) { - btn.classList.remove('border-blue-600', 'bg-blue-50'); - btn.classList.add('border-gray-300', 'bg-white'); - } - }); - this.classList.remove('border-gray-300', 'bg-white'); - this.classList.add('border-blue-600', 'bg-blue-50'); - }); - }); - } - - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createLanguageSelector); - } else { - createLanguageSelector(); - } - - // Re-initialize when language changes (to update active state) - if (window.I18n) { - const originalSetLanguage = window.I18n.setLanguage; - window.I18n.setLanguage = function(lang) { - originalSetLanguage.call(window.I18n, lang); - createLanguageSelector(); // Refresh selector with new active language - }; - } -})(); diff --git a/public/js/components/navbar.js b/public/js/components/navbar.js deleted file mode 100644 index f8fd5e62..00000000 --- a/public/js/components/navbar.js +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Tractatus Framework - Responsive Navbar Component - * Consistent, mobile-friendly navigation across all pages - */ - -class TractatusNavbar { - constructor() { - this.mobileMenuOpen = false; - this.init(); - } - - init() { - this.render(); - this.attachEventListeners(); - this.setActivePageIndicator(); - } - - render() { - const navHTML = ` - - `; - - // Always insert navbar at the very beginning of body - // Check if there's already a tractatus navbar (to avoid duplicates) - const existingNavbar = document.querySelector('nav.bg-white.border-b.border-gray-200.sticky'); - if (existingNavbar) { - existingNavbar.outerHTML = navHTML; - } else { - const placeholder = document.getElementById('navbar-placeholder'); - if (placeholder) { - placeholder.outerHTML = navHTML; - } else { - document.body.insertAdjacentHTML('afterbegin', navHTML); - } - } - } - - attachEventListeners() { - // Mobile Menu (Navigation Drawer) - const mobileMenuBtn = document.getElementById('mobile-menu-btn'); - const mobileMenuCloseBtn = document.getElementById('mobile-menu-close-btn'); - const mobileMenu = document.getElementById('mobile-menu'); - const mobileMenuPanel = document.getElementById('mobile-menu-panel'); - const mobileMenuBackdrop = document.getElementById('mobile-menu-backdrop'); - - const toggleMobileMenu = () => { - this.mobileMenuOpen = !this.mobileMenuOpen; - - if (this.mobileMenuOpen) { - // Open: Show menu and slide panel in from right - mobileMenu.classList.remove('hidden'); - // Use setTimeout to ensure display change happens before animation - setTimeout(() => { - mobileMenuPanel.classList.remove('translate-x-full'); - mobileMenuPanel.classList.add('translate-x-0'); - }, 10); - document.body.style.overflow = 'hidden'; // Prevent scrolling when menu is open - } else { - // Close: Slide panel out to right - mobileMenuPanel.classList.remove('translate-x-0'); - mobileMenuPanel.classList.add('translate-x-full'); - // Hide menu after animation completes (300ms) - setTimeout(() => { - mobileMenu.classList.add('hidden'); - }, 300); - document.body.style.overflow = ''; - } - }; - - // Initialize panel in hidden state (off-screen to the right) - if (mobileMenuPanel) { - mobileMenuPanel.classList.add('translate-x-full'); - } - - if (mobileMenuBtn) { - mobileMenuBtn.addEventListener('click', toggleMobileMenu); - } - - if (mobileMenuCloseBtn) { - mobileMenuCloseBtn.addEventListener('click', toggleMobileMenu); - } - - if (mobileMenuBackdrop) { - mobileMenuBackdrop.addEventListener('click', toggleMobileMenu); - } - - // Close mobile menu on navigation - const mobileLinks = document.querySelectorAll('#mobile-menu a'); - mobileLinks.forEach(link => { - link.addEventListener('click', () => { - if (this.mobileMenuOpen) { - toggleMobileMenu(); - } - }); - }); - } - - setActivePageIndicator() { - // Get current page path - const currentPath = window.location.pathname; - - // Normalize paths (handle both /page.html and /page) - const normalizePath = (path) => { - if (path === '/' || path === '/index.html') return '/'; - return path.replace('.html', '').replace(/\/$/, ''); - }; - - const normalizedCurrent = normalizePath(currentPath); - - // Find all navigation links in mobile menu - const mobileLinks = document.querySelectorAll('#mobile-menu a'); - - mobileLinks.forEach(link => { - const linkPath = link.getAttribute('href'); - const normalizedLink = normalizePath(linkPath); - - if (normalizedLink === normalizedCurrent) { - // Add active styling with brand colors - link.classList.add('border-l-4', 'bg-sky-50'); - link.style.borderLeftColor = 'var(--tractatus-core-end)'; - link.style.color = 'var(--tractatus-core-end)'; - link.classList.remove('text-gray-700'); - } - }); - } -} - -// Auto-initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => new TractatusNavbar()); -} else { - new TractatusNavbar(); -} diff --git a/public/js/components/toc.js b/public/js/components/toc.js deleted file mode 100644 index 0fa1dd2d..00000000 --- a/public/js/components/toc.js +++ /dev/null @@ -1,268 +0,0 @@ -/** - * Table of Contents Component - * Tractatus Framework - Phase 3: Interactive Documentation - * - * Creates sticky TOC sidebar on desktop, collapsible on mobile - * Highlights current section on scroll - * Smooth scroll to sections - */ - -class TableOfContents { - constructor(options = {}) { - this.contentSelector = options.contentSelector || '#document-viewer'; - this.tocSelector = options.tocSelector || '#table-of-contents'; - this.headingSelector = options.headingSelector || 'h1, h2, h3'; - this.activeClass = 'toc-active'; - this.collapsedClass = 'toc-collapsed'; - - this.headings = []; - this.tocLinks = []; - this.currentActiveIndex = -1; - - this.init(); - } - - init() { - // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => this.build()); - } else { - this.build(); - } - - console.log('[TOC] Initialized'); - } - - build() { - const tocContainer = document.querySelector(this.tocSelector); - if (!tocContainer) { - console.warn('[TOC] TOC container not found:', this.tocSelector); - return; - } - - const content = document.querySelector(this.contentSelector); - if (!content) { - console.warn('[TOC] Content container not found:', this.contentSelector); - return; - } - - // Find all headings in content - this.headings = Array.from(content.querySelectorAll(this.headingSelector)); - - if (this.headings.length === 0) { - console.log('[TOC] No headings found, hiding TOC'); - tocContainer.style.display = 'none'; - return; - } - - console.log(`[TOC] Found ${this.headings.length} headings`); - - // Generate IDs for headings if they don't have them - this.headings.forEach((heading, index) => { - if (!heading.id) { - heading.id = `toc-heading-${index}`; - } - }); - - // Build TOC HTML - const tocHTML = this.buildTOCHTML(); - tocContainer.innerHTML = tocHTML; - - // Store TOC links - this.tocLinks = Array.from(tocContainer.querySelectorAll('a')); - - // Add scroll spy - this.initScrollSpy(); - - // Add smooth scroll - this.initSmoothScroll(); - - // Add mobile toggle functionality - this.initMobileToggle(); - } - - buildTOCHTML() { - let html = ''; - - return html; - } - - initScrollSpy() { - // Use Intersection Observer for better performance - const observerOptions = { - rootMargin: '-20% 0px -35% 0px', - threshold: 0 - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const id = entry.target.id; - this.setActiveLink(id); - } - }); - }, observerOptions); - - // Observe all headings - this.headings.forEach(heading => { - observer.observe(heading); - }); - - console.log('[TOC] Scroll spy initialized'); - } - - setActiveLink(targetId) { - // Remove active class from all links - this.tocLinks.forEach(link => { - link.classList.remove(this.activeClass); - link.classList.remove('border-blue-600', 'text-blue-600', 'font-semibold'); - link.classList.add('border-transparent', 'text-gray-700'); - }); - - // Add active class to current link - const activeLink = this.tocLinks.find(link => link.dataset.target === targetId); - if (activeLink) { - activeLink.classList.add(this.activeClass); - activeLink.classList.remove('border-transparent', 'text-gray-700'); - activeLink.classList.add('border-blue-600', 'text-blue-600', 'font-semibold'); - } - } - - initSmoothScroll() { - this.tocLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const targetId = link.dataset.target; - const targetElement = document.getElementById(targetId); - - if (targetElement) { - // Smooth scroll to target - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'start' - }); - - // Update URL hash without jumping - if (history.pushState) { - history.pushState(null, null, `#${targetId}`); - } else { - window.location.hash = targetId; - } - - // On mobile, collapse TOC after clicking - if (window.innerWidth < 768) { - this.collapseTOC(); - } - } - }); - }); - - console.log('[TOC] Smooth scroll initialized'); - } - - initMobileToggle() { - const toggleButton = document.getElementById('toc-toggle'); - const tocList = document.getElementById('toc-list'); - - if (!toggleButton || !tocList) return; - - toggleButton.addEventListener('click', () => { - const isCollapsed = tocList.classList.contains('hidden'); - - if (isCollapsed) { - this.expandTOC(); - } else { - this.collapseTOC(); - } - }); - - // Start collapsed on mobile - if (window.innerWidth < 768) { - this.collapseTOC(); - } - - console.log('[TOC] Mobile toggle initialized'); - } - - collapseTOC() { - const tocList = document.getElementById('toc-list'); - const toggleButton = document.getElementById('toc-toggle'); - - if (tocList) { - tocList.classList.add('hidden'); - } - - if (toggleButton) { - const svg = toggleButton.querySelector('svg'); - if (svg) { - svg.style.transform = 'rotate(0deg)'; - } - } - } - - expandTOC() { - const tocList = document.getElementById('toc-list'); - const toggleButton = document.getElementById('toc-toggle'); - - if (tocList) { - tocList.classList.remove('hidden'); - } - - if (toggleButton) { - const svg = toggleButton.querySelector('svg'); - if (svg) { - svg.style.transform = 'rotate(180deg)'; - } - } - } - - // Public method to rebuild TOC (useful for dynamically loaded content) - rebuild() { - this.build(); - } -} - -// Auto-initialize when script loads (if TOC container exists) -if (typeof window !== 'undefined') { - window.tocInstance = new TableOfContents(); - - // Listen for custom event from document viewer for dynamic content - document.addEventListener('documentLoaded', () => { - console.log('[TOC] Document loaded, rebuilding TOC'); - window.tocInstance.rebuild(); - }); -} - -// Export for module usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = TableOfContents; -} diff --git a/public/js/docs-app.js b/public/js/docs-app.js deleted file mode 100644 index 9b5ae47b..00000000 --- a/public/js/docs-app.js +++ /dev/null @@ -1,625 +0,0 @@ -let documents = []; -let currentDocument = null; -let documentCards = null; - -// Initialize card-based viewer -if (typeof DocumentCards !== 'undefined') { - documentCards = new DocumentCards('document-content'); -} - -// Document categorization - Granular categories for better organization -const CATEGORIES = { - 'getting-started': { - label: '🚀 Getting Started', - icon: '🚀', - description: 'Introduction, core concepts, and quick start guides', - order: 1, - color: 'blue', - bgColor: 'bg-blue-50', - borderColor: 'border-l-4 border-blue-500', - textColor: 'text-blue-700', - collapsed: false - }, - 'technical-reference': { - label: '🔌 Technical Reference', - icon: '🔌', - description: 'API docs, implementation guides, code examples', - order: 2, - color: 'green', - bgColor: 'bg-green-50', - borderColor: 'border-l-4 border-green-500', - textColor: 'text-green-700', - collapsed: true - }, - 'research-theory': { - label: '🔬 Theory & Research', - icon: '🔬', - description: 'Research papers, theoretical foundations, academic content', - order: 3, - color: 'purple', - bgColor: 'bg-purple-50', - borderColor: 'border-l-4 border-purple-500', - textColor: 'text-purple-700', - collapsed: true - }, - 'advanced-topics': { - label: '🎓 Advanced Topics', - icon: '🎓', - description: 'Value pluralism, deep dives, comparative analysis', - order: 4, - color: 'teal', - bgColor: 'bg-teal-50', - borderColor: 'border-l-4 border-teal-500', - textColor: 'text-teal-700', - collapsed: true - }, - 'case-studies': { - label: '📊 Case Studies', - icon: '📊', - description: 'Real-world examples, failure modes, success stories', - order: 5, - color: 'amber', - bgColor: 'bg-amber-50', - borderColor: 'border-l-4 border-amber-500', - textColor: 'text-amber-700', - collapsed: true - }, - 'business-leadership': { - label: '💼 Business & Leadership', - icon: '💼', - description: 'Business cases, executive briefs, ROI analysis', - order: 6, - color: 'pink', - bgColor: 'bg-pink-50', - borderColor: 'border-l-4 border-pink-500', - textColor: 'text-pink-700', - collapsed: true - } -}; - -// Documents to hide (internal/confidential) -const HIDDEN_DOCS = [ - 'security-audit-report', - 'koha-production-deployment', - 'koha-stripe-payment', - 'appendix-e-contact', - 'cover-letter' -]; - -// Categorize a document using database category field -// New granular category system for better document organization -function categorizeDocument(doc) { - const slug = doc.slug.toLowerCase(); - - // Skip hidden documents - if (HIDDEN_DOCS.some(hidden => slug.includes(hidden))) { - return null; - } - - // Use category from database - const category = doc.category || 'downloads-resources'; - - // Validate category exists in CATEGORIES constant - if (CATEGORIES[category]) { - return category; - } - - // Fallback to downloads-resources for uncategorized - console.warn(`Document "${doc.title}" has invalid category "${category}", using fallback`); - return 'downloads-resources'; -} - -// Group documents by category -function groupDocuments(docs) { - const grouped = {}; - - // Initialize all categories - Object.keys(CATEGORIES).forEach(key => { - grouped[key] = []; - }); - - // Categorize each document (already sorted by order from API) - docs.forEach(doc => { - const category = categorizeDocument(doc); - if (category && grouped[category]) { - grouped[category].push(doc); - } - }); - - return grouped; -} - -// Render document link with download button -function renderDocLink(doc, isHighlighted = false) { - const highlightClass = isHighlighted ? 'text-blue-700 bg-blue-50 border border-blue-200' : ''; - - // Determine if PDF download is available and get PDF path - // First check if document has explicit download_formats.pdf - let pdfPath = null; - let hasPDF = false; - - if (doc.download_formats && doc.download_formats.pdf) { - pdfPath = doc.download_formats.pdf; - hasPDF = true; - } else if (!doc.slug.includes('api-reference-complete') && - !doc.slug.includes('openapi-specification') && - !doc.slug.includes('api-javascript-examples') && - !doc.slug.includes('api-python-examples') && - !doc.slug.includes('technical-architecture-diagram')) { - // Fallback to default /downloads/ path for documents that typically have PDFs - pdfPath = `/downloads/${doc.slug}.pdf`; - hasPDF = true; - } - - // Add download button styling - const paddingClass = hasPDF ? 'pr-10' : 'pr-3'; - - return ` -
- - ${hasPDF ? ` - - - - - - ` : ''} -
- `; -} - -// Load document list -async function loadDocuments() { - try { - // Fetch public documents - const response = await fetch('/api/documents'); - const data = await response.json(); - documents = data.documents || []; - - // Fetch archived documents - const archivedResponse = await fetch('/api/documents/archived'); - const archivedData = await archivedResponse.json(); - const archivedDocuments = archivedData.documents || []; - - const listEl = document.getElementById('document-list'); - if (documents.length === 0 && archivedDocuments.length === 0) { - listEl.innerHTML = '
No documents available
'; - return; - } - - // Group documents by category - const grouped = groupDocuments(documents); - - let html = ''; - - // Render categories in order - const sortedCategories = Object.entries(CATEGORIES) - .sort((a, b) => a[1].order - b[1].order); - - sortedCategories.forEach(([categoryId, category]) => { - const docs = grouped[categoryId] || []; - if (docs.length === 0) return; - - const isCollapsed = category.collapsed || false; - - // Category header - html += ` -
- -
- `; - - // Render documents in category - docs.forEach(doc => { - // Highlight the first document in Getting Started category - const isHighlighted = categoryId === 'getting-started' && doc.order === 1; - html += renderDocLink(doc, isHighlighted); - }); - - html += ` -
-
- `; - }); - - // Add Archives section if there are archived documents - if (archivedDocuments.length > 0) { - html += ` -
- -
- `; - - // Render archived documents - archivedDocuments.forEach(doc => { - html += renderDocLink(doc, false); - // Add archive note if available - if (doc.archiveNote) { - html += `
${doc.archiveNote}
`; - } - }); - - html += ` -
-
- `; - } - - listEl.innerHTML = html; - - // Apply collapsed state to categories (CSP-compliant - no inline styles) - listEl.querySelectorAll('.category-docs[data-collapsed="true"]').forEach(docsEl => { - docsEl.style.display = 'none'; - }); - listEl.querySelectorAll('.category-toggle[data-collapsed="true"] .category-arrow').forEach(arrowEl => { - arrowEl.style.transform = 'rotate(-90deg)'; - }); - - // Add event delegation for document links - listEl.addEventListener('click', function(e) { - // Check for download link first (prevent document load when clicking download) - const downloadLink = e.target.closest('.doc-download-link'); - if (downloadLink) { - e.stopPropagation(); - return; - } - - const button = e.target.closest('.doc-link'); - if (button && button.dataset.slug) { - e.preventDefault(); - loadDocument(button.dataset.slug); - return; - } - - // Category toggle - const toggle = e.target.closest('.category-toggle'); - if (toggle) { - const categoryId = toggle.dataset.category; - const docsEl = listEl.querySelector(`.category-docs[data-category="${categoryId}"]`); - const arrowEl = toggle.querySelector('.category-arrow'); - - if (docsEl.style.display === 'none') { - docsEl.style.display = 'block'; - arrowEl.style.transform = 'rotate(0deg)'; - } else { - docsEl.style.display = 'none'; - arrowEl.style.transform = 'rotate(-90deg)'; - } - } - }); - - // Check for URL parameter to auto-load document or category - const urlParams = new URLSearchParams(window.location.search); - const docParam = urlParams.get('doc'); - const categoryParam = urlParams.get('category'); - - // Priority 1: Load specific document by slug if provided - if (docParam) { - const doc = documents.find(d => d.slug === docParam); - if (doc) { - // Find and expand the category containing this document - const docCategory = categorizeDocument(doc); - if (docCategory) { - const categoryDocsEl = listEl.querySelector(`.category-docs[data-category="${docCategory}"]`); - const categoryArrowEl = listEl.querySelector(`.category-toggle[data-category="${docCategory}"] .category-arrow`); - - if (categoryDocsEl) { - categoryDocsEl.style.display = 'block'; - if (categoryArrowEl) { - categoryArrowEl.style.transform = 'rotate(0deg)'; - } - } - } - - // Load the requested document - loadDocument(docParam); - } else { - console.warn(`Document with slug "${docParam}" not found`); - } - } - // Priority 2: Load category if provided but no specific document - else if (categoryParam && grouped[categoryParam] && grouped[categoryParam].length > 0) { - // Expand the specified category - const categoryDocsEl = listEl.querySelector(`.category-docs[data-category="${categoryParam}"]`); - const categoryArrowEl = listEl.querySelector(`.category-toggle[data-category="${categoryParam}"] .category-arrow`); - - if (categoryDocsEl) { - categoryDocsEl.style.display = 'block'; - if (categoryArrowEl) { - categoryArrowEl.style.transform = 'rotate(0deg)'; - } - } - - // Load first document in the category - const firstDoc = grouped[categoryParam][0]; - if (firstDoc) { - loadDocument(firstDoc.slug); - } - } - // Priority 3: Default behavior - else { - // Default: Auto-load first document in "Getting Started" category (order: 1) - const gettingStartedDocs = grouped['getting-started'] || []; - if (gettingStartedDocs.length > 0) { - // Load the first document (order: 1) if available - const firstDoc = gettingStartedDocs.find(d => d.order === 1); - if (firstDoc) { - loadDocument(firstDoc.slug); - } else { - loadDocument(gettingStartedDocs[0].slug); - } - } else if (documents.length > 0) { - // Fallback to first available document in any category - const firstCategory = sortedCategories.find(([catId]) => grouped[catId] && grouped[catId].length > 0); - if (firstCategory) { - loadDocument(grouped[firstCategory[0]][0].slug); - } - } - } - } catch (error) { - console.error('Error loading documents:', error); - document.getElementById('document-list').innerHTML = - '
Error loading documents
'; - } -} - -// Load specific document -let isLoading = false; - -async function loadDocument(slug) { - // Prevent multiple simultaneous loads - if (isLoading) return; - - try { - isLoading = true; - - // Show loading state - const contentEl = document.getElementById('document-content'); - contentEl.innerHTML = ` -
- - - - -

Loading document...

-
- `; - - const response = await fetch(`/api/documents/${slug}`); - const data = await response.json(); - - if (!data.success) { - throw new Error(data.error || 'Failed to load document'); - } - - currentDocument = data.document; - - // Update active state - document.querySelectorAll('.doc-link').forEach(el => { - if (el.dataset.slug === slug) { - el.classList.add('bg-blue-100', 'text-blue-900'); - } else { - el.classList.remove('bg-blue-100', 'text-blue-900'); - } - }); - - // Render with card-based viewer if available and document has sections - if (documentCards && currentDocument.sections && currentDocument.sections.length > 0) { - documentCards.render(currentDocument); - } else { - // Fallback to traditional view with header - const hasToC = currentDocument.toc && currentDocument.toc.length > 0; - - // Check if PDF is available and get PDF path - let pdfPath = null; - let hasPDF = false; - - if (currentDocument.download_formats && currentDocument.download_formats.pdf) { - pdfPath = currentDocument.download_formats.pdf; - hasPDF = true; - } else if (!currentDocument.slug.includes('api-reference-complete') && - !currentDocument.slug.includes('openapi-specification') && - !currentDocument.slug.includes('api-javascript-examples') && - !currentDocument.slug.includes('api-python-examples') && - !currentDocument.slug.includes('technical-architecture-diagram')) { - pdfPath = `/downloads/${currentDocument.slug}.pdf`; - hasPDF = true; - } - - let headerHTML = ` -
-

${currentDocument.title}

-
- ${hasToC ? ` - - ` : ''} - ${hasPDF ? ` - - - - - - ` : ''} -
-
- `; - - // Remove duplicate title H1 from content (it's already in header) - let contentHtml = currentDocument.content_html; - const firstH1Match = contentHtml.match(/]*>.*?<\/h1>/); - if (firstH1Match) { - contentHtml = contentHtml.replace(firstH1Match[0], ''); - } - - contentEl.innerHTML = headerHTML + ` -
- ${contentHtml} -
- `; - } - - // Add ToC button event listener (works for both card and traditional views) - setTimeout(() => { - const tocButton = document.getElementById('toc-button'); - if (tocButton) { - tocButton.addEventListener('click', () => openToCModal()); - } - }, 100); - - // Mobile navigation: Add document-active class to show document view - document.body.classList.add('document-active'); - - // Scroll to top - window.scrollTo({ top: 0, behavior: 'smooth' }); - - } catch (error) { - console.error('Error loading document:', error); - document.getElementById('document-content').innerHTML = ` -
- - - -

Error loading document

-

${error.message}

-
- `; - } finally { - isLoading = false; - } -} - -// Open ToC modal -function openToCModal() { - if (!currentDocument || !currentDocument.toc || currentDocument.toc.length === 0) { - return; - } - - const modal = document.getElementById('toc-modal'); - if (!modal) return; - - // Render ToC content - const tocContent = document.getElementById('toc-modal-content'); - - const tocHTML = currentDocument.toc - .filter(item => item.level <= 3) // Only show H1, H2, H3 - .map(item => { - return ` - - ${item.title} - - `; - }).join(''); - - tocContent.innerHTML = tocHTML; - - // Show modal - modal.classList.add('show'); - - // Prevent body scroll and reset modal content scroll - document.body.style.overflow = 'hidden'; - tocContent.scrollTop = 0; - - // Add event listeners to ToC links - tocContent.querySelectorAll('.toc-link').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const targetId = link.getAttribute('href').substring(1); - const targetEl = document.getElementById(targetId); - if (targetEl) { - closeToCModal(); - setTimeout(() => { - targetEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, 200); - } - }); - }); -} - -// Close ToC modal -function closeToCModal() { - const modal = document.getElementById('toc-modal'); - if (modal) { - modal.classList.remove('show'); - document.body.style.overflow = ''; - } -} - -// Initialize -loadDocuments(); - -// Add ESC key listener for closing modal -document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - closeToCModal(); - } -}); - -// Add close button listener for ToC modal (script loads after DOM, so elements exist) -const closeButton = document.getElementById('toc-close-button'); -if (closeButton) { - closeButton.addEventListener('click', closeToCModal); -} - -// Click outside modal to close -const modal = document.getElementById('toc-modal'); -if (modal) { - modal.addEventListener('click', function(e) { - if (e.target === this) { - closeToCModal(); - } - }); -} - -// Mobile navigation: Back to documents button -const backButton = document.getElementById('back-to-docs-btn'); -if (backButton) { - backButton.addEventListener('click', function() { - // Remove document-active class to show sidebar - document.body.classList.remove('document-active'); - - // Scroll to top - window.scrollTo({ top: 0, behavior: 'smooth' }); - }); -} diff --git a/public/js/docs-search-enhanced.js b/public/js/docs-search-enhanced.js deleted file mode 100644 index d00740bb..00000000 --- a/public/js/docs-search-enhanced.js +++ /dev/null @@ -1,650 +0,0 @@ -/** - * Docs Search Enhancement Module - * Provides faceted search, filters, history, and keyboard navigation - * CSP Compliant - No inline scripts or event handlers - */ - -(function() { - 'use strict'; - - // Configuration - const CONFIG = { - DEBOUNCE_DELAY: 300, - MAX_SEARCH_HISTORY: 10, - SEARCH_HISTORY_KEY: 'tractatus_search_history', - MIN_QUERY_LENGTH: 2 - }; - - // State - let searchTimeout = null; - let currentFilters = { - query: '', - quadrant: '', - persistence: '', - audience: '' - }; - let searchHistory = []; - let selectedResultIndex = -1; - let searchResults = []; - - // DOM Elements - const elements = { - searchInput: null, - quadrantFilter: null, - persistenceFilter: null, - audienceFilter: null, - clearFiltersBtn: null, - searchTipsBtn: null, - searchTipsModal: null, - searchTipsCloseBtn: null, - searchResultsPanel: null, - searchResultsList: null, - closeSearchResults: null, - searchResultsSummary: null, - searchResultsCount: null, - searchHistoryContainer: null, - searchHistory: null, - searchModal: null, - openSearchModalBtn: null, - searchModalCloseBtn: null, - searchResultsModal: null, - searchResultsListModal: null - }; - - /** - * Initialize the search enhancement module - */ - function init() { - // Get DOM elements - elements.searchInput = document.getElementById('docs-search-input'); - elements.quadrantFilter = document.getElementById('filter-quadrant'); - elements.persistenceFilter = document.getElementById('filter-persistence'); - elements.audienceFilter = document.getElementById('filter-audience'); - elements.clearFiltersBtn = document.getElementById('clear-filters-btn'); - elements.searchTipsBtn = document.getElementById('search-tips-btn'); - elements.searchTipsModal = document.getElementById('search-tips-modal'); - elements.searchTipsCloseBtn = document.getElementById('search-tips-close-btn'); - elements.searchResultsPanel = document.getElementById('search-results-panel'); - elements.searchResultsList = document.getElementById('search-results-list'); - elements.closeSearchResults = document.getElementById('close-search-results'); - elements.searchResultsSummary = document.getElementById('search-results-summary'); - elements.searchResultsCount = document.getElementById('search-results-count'); - elements.searchHistoryContainer = document.getElementById('search-history-container'); - elements.searchHistory = document.getElementById('search-history'); - elements.searchModal = document.getElementById('search-modal'); - elements.openSearchModalBtn = document.getElementById('open-search-modal-btn'); - elements.searchModalCloseBtn = document.getElementById('search-modal-close-btn'); - elements.searchResultsModal = document.getElementById('search-results-modal'); - elements.searchResultsListModal = document.getElementById('search-results-list-modal'); - - // Check if essential elements exist - if (!elements.searchInput || !elements.searchModal) { - console.warn('Search elements not found - search enhancement disabled'); - return; - } - - // Load search history from localStorage - loadSearchHistory(); - - // Attach event listeners - attachEventListeners(); - - // Display search history if available - renderSearchHistory(); - } - - /** - * Attach event listeners (CSP compliant - no inline handlers) - */ - function attachEventListeners() { - // Search modal open/close - if (elements.openSearchModalBtn) { - elements.openSearchModalBtn.addEventListener('click', openSearchModal); - } - if (elements.searchModalCloseBtn) { - elements.searchModalCloseBtn.addEventListener('click', closeSearchModal); - } - if (elements.searchModal) { - elements.searchModal.addEventListener('click', function(e) { - if (e.target === elements.searchModal) { - closeSearchModal(); - } - }); - } - - // Search input - debounced - if (elements.searchInput) { - elements.searchInput.addEventListener('input', handleSearchInput); - elements.searchInput.addEventListener('keydown', handleSearchKeydown); - } - - // Filter dropdowns - if (elements.quadrantFilter) { - elements.quadrantFilter.addEventListener('change', handleFilterChange); - } - if (elements.persistenceFilter) { - elements.persistenceFilter.addEventListener('change', handleFilterChange); - } - if (elements.audienceFilter) { - elements.audienceFilter.addEventListener('change', handleFilterChange); - } - - // Clear filters button - if (elements.clearFiltersBtn) { - elements.clearFiltersBtn.addEventListener('click', clearFilters); - } - - // Search tips button - if (elements.searchTipsBtn) { - elements.searchTipsBtn.addEventListener('click', openSearchTipsModal); - } - if (elements.searchTipsCloseBtn) { - elements.searchTipsCloseBtn.addEventListener('click', closeSearchTipsModal); - } - if (elements.searchTipsModal) { - elements.searchTipsModal.addEventListener('click', function(e) { - if (e.target === elements.searchTipsModal) { - closeSearchTipsModal(); - } - }); - } - - // Close search results - if (elements.closeSearchResults) { - elements.closeSearchResults.addEventListener('click', closeSearchResultsPanel); - } - - // Global keyboard shortcuts - document.addEventListener('keydown', handleGlobalKeydown); - - // Escape key to close modals - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { - closeSearchTipsModal(); - closeSearchModal(); - } - }); - } - - /** - * Handle search input with debounce - */ - function handleSearchInput(e) { - const query = e.target.value.trim(); - - // Clear previous timeout - if (searchTimeout) { - clearTimeout(searchTimeout); - } - - // Debounce search - searchTimeout = setTimeout(() => { - currentFilters.query = query; - performSearch(); - }, CONFIG.DEBOUNCE_DELAY); - } - - /** - * Handle keyboard navigation in search input - */ - function handleSearchKeydown(e) { - if (e.key === 'Enter') { - e.preventDefault(); - const query = e.target.value.trim(); - if (query) { - currentFilters.query = query; - performSearch(); - } - } else if (e.key === 'Escape') { - closeSearchResultsPanel(); - closeSearchModal(); - e.target.blur(); - } else if (e.key === 'ArrowDown') { - e.preventDefault(); - navigateResults('down'); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - navigateResults('up'); - } - } - - /** - * Handle filter changes - */ - function handleFilterChange() { - currentFilters.quadrant = elements.quadrantFilter ? elements.quadrantFilter.value : ''; - currentFilters.persistence = elements.persistenceFilter ? elements.persistenceFilter.value : ''; - currentFilters.audience = elements.audienceFilter ? elements.audienceFilter.value : ''; - - performSearch(); - } - - /** - * Clear all filters - */ - function clearFilters() { - if (elements.searchInput) elements.searchInput.value = ''; - if (elements.quadrantFilter) elements.quadrantFilter.value = ''; - if (elements.persistenceFilter) elements.persistenceFilter.value = ''; - if (elements.audienceFilter) elements.audienceFilter.value = ''; - - currentFilters = { - query: '', - quadrant: '', - persistence: '', - audience: '' - }; - - closeSearchResultsPanel(); - } - - /** - * Perform search with current filters - */ - async function performSearch() { - const { query, quadrant, persistence, audience } = currentFilters; - - // If no query and no filters, don't search - if (!query && !quadrant && !persistence && !audience) { - closeSearchResultsPanel(); - return; - } - - // Build query params - const params = new URLSearchParams(); - if (query) params.append('q', query); - if (quadrant) params.append('quadrant', quadrant); - if (persistence) params.append('persistence', persistence); - if (audience) params.append('audience', audience); - - try { - const startTime = performance.now(); - const response = await fetch(`/api/documents/search?${params.toString()}`); - const endTime = performance.now(); - const duration = Math.round(endTime - startTime); - - const data = await response.json(); - - if (data.success) { - searchResults = data.documents || []; - renderSearchResults(data, duration); - - // Save to search history if query exists - if (query) { - addToSearchHistory(query); - } - } else { - showError('Search failed. Please try again.'); - } - } catch (error) { - console.error('Search error:', error); - showError('Search failed. Please check your connection.'); - } - } - - /** - * Render search results - */ - function renderSearchResults(data, duration) { - // Use modal list if available, otherwise fall back to panel list - const targetList = elements.searchResultsListModal || elements.searchResultsList; - const targetContainer = elements.searchResultsModal || elements.searchResultsPanel; - - if (!targetList) return; - - const { documents, count, total, filters } = data; - - // Show results container - if (targetContainer) { - targetContainer.classList.remove('hidden'); - } - - // Update summary - if (elements.searchResultsSummary && elements.searchResultsCount) { - elements.searchResultsSummary.classList.remove('hidden'); - - let summaryText = `Found ${total} document${total !== 1 ? 's' : ''}`; - if (duration) { - summaryText += ` (${duration}ms)`; - } - - const activeFilters = []; - if (filters.quadrant) activeFilters.push(`Quadrant: ${filters.quadrant}`); - if (filters.persistence) activeFilters.push(`Persistence: ${filters.persistence}`); - if (filters.audience) activeFilters.push(`Audience: ${filters.audience}`); - - if (activeFilters.length > 0) { - summaryText += ` • Filters: ${activeFilters.join(', ')}`; - } - - elements.searchResultsCount.textContent = summaryText; - } - - // Render results - if (documents.length === 0) { - targetList.innerHTML = ` -
- - - -

No documents found

-

Try adjusting your search terms or filters

-
- `; - return; - } - - const resultsHTML = documents.map((doc, index) => { - const badges = []; - if (doc.quadrant) badges.push(`${doc.quadrant}`); - if (doc.persistence) badges.push(`${doc.persistence}`); - if (doc.audience) badges.push(`${doc.audience}`); - - // Highlight query terms in title (simple highlighting) - let highlightedTitle = doc.title; - if (currentFilters.query) { - const regex = new RegExp(`(${escapeRegex(currentFilters.query)})`, 'gi'); - highlightedTitle = doc.title.replace(regex, '$1'); - } - - return ` -
-
-
-

${highlightedTitle}

-
- ${badges.join('')} -
-

${doc.metadata?.description || 'Framework documentation'}

-
- - - - - -
-
- `; - }).join(''); - - targetList.innerHTML = resultsHTML; - - // Attach click handlers to results - document.querySelectorAll('.search-result-item').forEach(item => { - item.addEventListener('click', function(e) { - // Don't navigate if clicking download link - if (e.target.closest('a[href*="/downloads/"]')) { - return; - } - - const slug = this.dataset.slug; - if (slug && typeof loadDocument === 'function') { - loadDocument(slug); - closeSearchResultsPanel(); - closeSearchModal(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }); - }); - - // Reset selected index - selectedResultIndex = -1; - } - - /** - * Navigate search results with keyboard - */ - function navigateResults(direction) { - if (searchResults.length === 0) return; - - if (direction === 'down') { - selectedResultIndex = Math.min(selectedResultIndex + 1, searchResults.length - 1); - } else if (direction === 'up') { - selectedResultIndex = Math.max(selectedResultIndex - 1, -1); - } - - // Re-render to show selection - if (searchResults.length > 0) { - const items = document.querySelectorAll('.search-result-item'); - items.forEach((item, index) => { - if (index === selectedResultIndex) { - item.classList.add('border-blue-500', 'bg-blue-50'); - item.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } else { - item.classList.remove('border-blue-500', 'bg-blue-50'); - } - }); - - // If Enter key pressed on selected result - if (selectedResultIndex >= 0) { - const selectedSlug = searchResults[selectedResultIndex].slug; - // Store for Enter key handler - document.addEventListener('keydown', function enterHandler(e) { - if (e.key === 'Enter' && selectedResultIndex >= 0) { - if (typeof loadDocument === 'function') { - loadDocument(selectedSlug); - closeSearchResultsPanel(); - } - document.removeEventListener('keydown', enterHandler); - } - }, { once: true }); - } - } - } - - /** - * Close search results panel - */ - function closeSearchResultsPanel() { - if (elements.searchResultsPanel) { - elements.searchResultsPanel.classList.add('hidden'); - } - if (elements.searchResultsSummary) { - elements.searchResultsSummary.classList.add('hidden'); - } - selectedResultIndex = -1; - searchResults = []; - } - - /** - * Show error message - */ - function showError(message) { - if (elements.searchResultsList) { - elements.searchResultsList.innerHTML = ` -
- - - -

${message}

-
- `; - } - } - - /** - * Open search tips modal - */ - function openSearchTipsModal() { - if (elements.searchTipsModal) { - elements.searchTipsModal.classList.remove('hidden'); - elements.searchTipsModal.classList.add('flex'); - document.body.style.overflow = 'hidden'; - } - } - - /** - * Close search tips modal - */ - function closeSearchTipsModal() { - if (elements.searchTipsModal) { - elements.searchTipsModal.classList.add('hidden'); - elements.searchTipsModal.classList.remove('flex'); - document.body.style.overflow = ''; - } - } - - /** - * Open search modal - */ - function openSearchModal() { - if (elements.searchModal) { - elements.searchModal.classList.remove('hidden'); - elements.searchModal.classList.add('show'); - document.body.style.overflow = 'hidden'; - - // Focus search input after modal opens - setTimeout(() => { - if (elements.searchInput) { - elements.searchInput.focus(); - } - }, 100); - } - } - - /** - * Close search modal - */ - function closeSearchModal() { - if (elements.searchModal) { - elements.searchModal.classList.remove('show'); - elements.searchModal.classList.add('hidden'); - document.body.style.overflow = ''; - - // Clear search when closing - clearFilters(); - - // Hide results - if (elements.searchResultsModal) { - elements.searchResultsModal.classList.add('hidden'); - } - if (elements.searchResultsSummary) { - elements.searchResultsSummary.classList.add('hidden'); - } - } - } - - /** - * Handle global keyboard shortcuts - */ - function handleGlobalKeydown(e) { - // Ctrl+K or Cmd+K to open search modal - if ((e.ctrlKey || e.metaKey) && e.key === 'k') { - e.preventDefault(); - openSearchModal(); - } - } - - /** - * Load search history from localStorage - */ - function loadSearchHistory() { - try { - const stored = localStorage.getItem(CONFIG.SEARCH_HISTORY_KEY); - if (stored) { - searchHistory = JSON.parse(stored); - } - } catch (error) { - console.warn('Failed to load search history:', error); - searchHistory = []; - } - } - - /** - * Save search history to localStorage - */ - function saveSearchHistory() { - try { - localStorage.setItem(CONFIG.SEARCH_HISTORY_KEY, JSON.stringify(searchHistory)); - } catch (error) { - console.warn('Failed to save search history:', error); - } - } - - /** - * Add query to search history - */ - function addToSearchHistory(query) { - if (!query || query.length < CONFIG.MIN_QUERY_LENGTH) return; - - // Remove duplicates - searchHistory = searchHistory.filter(item => item !== query); - - // Add to beginning - searchHistory.unshift(query); - - // Limit size - if (searchHistory.length > CONFIG.MAX_SEARCH_HISTORY) { - searchHistory = searchHistory.slice(0, CONFIG.MAX_SEARCH_HISTORY); - } - - saveSearchHistory(); - renderSearchHistory(); - } - - /** - * Render search history - */ - function renderSearchHistory() { - if (!elements.searchHistory || !elements.searchHistoryContainer) return; - - if (searchHistory.length === 0) { - elements.searchHistoryContainer.classList.add('hidden'); - return; - } - - elements.searchHistoryContainer.classList.remove('hidden'); - - const historyHTML = searchHistory.slice(0, 5).map(query => { - return ` - - `; - }).join(''); - - elements.searchHistory.innerHTML = historyHTML; - - // Attach click handlers - document.querySelectorAll('.search-history-item').forEach(item => { - item.addEventListener('click', function() { - const query = this.dataset.query; - if (elements.searchInput) { - elements.searchInput.value = query; - currentFilters.query = query; - performSearch(); - } - }); - }); - } - - /** - * Escape HTML to prevent XSS - */ - function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - /** - * Escape regex special characters - */ - function escapeRegex(text) { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } - - // Initialize when DOM is ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } - -})(); diff --git a/public/js/docs-viewer-app.js b/public/js/docs-viewer-app.js deleted file mode 100644 index 1b5543c1..00000000 --- a/public/js/docs-viewer-app.js +++ /dev/null @@ -1,35 +0,0 @@ -// Initialize document viewer -const viewer = new DocumentViewer('document-viewer'); - -// Load navigation -async function loadNavigation() { - try { - const response = await API.Documents.list({ limit: 50 }); - const nav = document.getElementById('doc-navigation'); - - if (response.success && response.documents) { - nav.innerHTML = response.documents.map(doc => ` - - ${doc.title} - - `).join(''); - } - } catch (error) { - console.error('Failed to load navigation:', error); - } -} - -// Setup routing -router - .on('/docs-viewer.html', async () => { - // Show default document - await viewer.render('introduction-to-the-tractatus-framework'); - }) - .on('/docs/:slug', async (params) => { - await viewer.render(params.slug); - }); - -// Initialize -loadNavigation(); diff --git a/public/js/faq.js b/public/js/faq.js deleted file mode 100644 index 4c346bf8..00000000 --- a/public/js/faq.js +++ /dev/null @@ -1,3352 +0,0 @@ -/** - * FAQ Page - Interactive search, filtering, and expandable Q&A - * Tractatus AI Safety Framework - */ - -const FAQ_DATA = [ - // IMPLEMENTER QUESTIONS - { - id: 19, - question: "Why not just use better prompts or a CLAUDE.md file?", - answer: `Better prompts and CLAUDE.md files are valuable but insufficient for production AI safety. Here's why Tractatus is necessary: - -**CLAUDE.md limitations:** -- **No enforcement**: Static documentation can be ignored under context pressure -- **No persistence**: Instructions may be lost during conversation compaction (200k token limit) -- **No audit trail**: No record of governance enforcement -- **No detection**: Can't catch pattern bias or instruction fade - -**Tractatus adds:** -- **Automated enforcement**: BoundaryEnforcer blocks values decisions before execution -- **Persistent storage**: Instructions classified and stored in .claude/instruction-history.json -- **Conflict detection**: CrossReferenceValidator prevents pattern bias (like the 27027 incident) -- **Real-time monitoring**: ContextPressureMonitor warns before degradation occurs - -**Validation context:** -Framework validated in 6-month, single-project deployment (~500 sessions with Claude Code). Pattern bias incidents prevented, values decisions consistently escalated to human approval, instructions maintained across session continuations. - -Operational metrics from controlled studies not yet available. This is early-stage research, not production-scale validation. - -Prompts guide behaviour. Tractatus enforces it architecturally.`, - audience: ['researcher', 'implementer'], - keywords: ['prompts', 'claude.md', 'enforcement', 'limitations', 'architecture'] - }, - { - id: 12, - question: "What's the performance overhead cost?", - answer: `Tractatus adds minimal overhead for comprehensive governance: - -**Estimated overhead: <10ms per operation** based on service architecture - -**Service-specific estimates:** -- BoundaryEnforcer: <5ms per check (rule lookup + validation) -- InstructionPersistenceClassifier: <10ms (classification + storage) -- CrossReferenceValidator: <15ms (query + validation) -- ContextPressureMonitor: <5ms (calculation) -- MetacognitiveVerifier: 50-200ms (selective, complex operations only) - -**Design trade-off:** -Governance services operate synchronously to ensure enforcement cannot be bypassed. This adds latency but provides architectural safety enforcement that asynchronous approaches cannot. - -**Development context:** -Framework validated in 6-month, single-project deployment. No systematic performance benchmarking conducted. Overhead estimates based on service architecture, not controlled studies. - -For production deployments where safety matters, minor latency is acceptable trade-off compared to risk of ungoverned AI decisions. Organisations should benchmark in their specific context.`, - audience: ['implementer', 'leader'], - keywords: ['performance', 'overhead', 'latency', 'cost', 'benchmarks', 'speed'] - }, - // RESEARCHER QUESTIONS - { - id: 27, - question: "Does Tractatus support multiple LLMs beyond Claude Code?", - answer: `Currently, Tractatus is optimized for Claude Code with plans for multi-model support: - -**Current implementation:** -- **Primary target**: Claude Code (Anthropic Sonnet 4.5) -- **Architecture**: Designed for 200k token context window -- **Integration**: Uses Bash, Read, Write, Edit tools native to Claude Code - -**Why Claude Code first:** -- Tool access for file system operations (.claude/ directory) -- Session continuity across compactions -- Native JSON parsing for governance rules -- Strong reasoning capabilities for metacognitive verification - -**Feasibility for other LLMs:** -✅ **Conceptually portable**: Governance principles (boundary enforcement, instruction persistence, pressure monitoring) apply to any LLM - -⚠️ **Implementation challenges:** -- Different context window sizes (GPT-4: 128k, Gemini: 1M) -- Tool access varies (function calling vs direct tools) -- Session management differs across platforms -- Classification accuracy depends on reasoning capability - -**Research in progress:** -See our feasibility study: [Research Scope: Feasibility of LLM-Integrated Tractatus Framework](/downloads/research-scope-feasibility-of-llm-integrated-tractatus-framework.pdf) - -**Roadmap for multi-model support:** -- Phase 1 (current): Claude Code production deployment -- Phase 2 (2026): OpenAI API integration -- Phase 3 (2026-2027): Gemini, local models (Llama 3) - -**If you need multi-model now**: Contact us to discuss custom implementation at research@agenticgovernance.digital`, - audience: ['researcher', 'implementer'], - keywords: ['multi-model', 'gpt-4', 'gemini', 'llama', 'openai', 'support', 'compatibility'] - }, - { - id: 13, - question: "How does Tractatus relate to Constitutional AI?", - answer: `Tractatus complements Constitutional AI with architectural enforcement: - -**Constitutional AI (Anthropic):** -- **Approach**: Train models with constitutional principles during RLHF -- **Layer**: Model weights and training data -- **Enforcement**: Behavioral tendency, not architectural enforcement -- **Strengths**: Deeply embedded values, broad coverage - -**Tractatus Framework:** -- **Approach**: Runtime governance layer on top of trained models -- **Layer**: Application architecture and session management -- **Enforcement**: Architectural blocking before action execution -- **Strengths**: Explicit enforcement, auditable, customizable per deployment - -**They work together:** - -\`\`\` -User instruction: "Change privacy policy to enable tracking" - ↓ -Constitutional AI (model level): - Trained to be cautious about privacy - May refuse autonomously - ↓ -Tractatus BoundaryEnforcer (architecture level): - Detects values decision (privacy) - BLOCKS action before execution - Escalates to human approval - Logs to audit trail -\`\`\` - -**Why both matter:** -- **Constitutional AI**: Prevents model from generating harmful content -- **Tractatus**: Prevents deployed system from executing harmful actions - -**Analogy:** -- Constitutional AI = Training a security guard to recognize threats -- Tractatus = Installing locks, alarms, and access control systems - -**Key difference:** -- Constitutional AI is opaque (can't explain why it refused) -- Tractatus is transparent (logs show which rule blocked which action) - -**For production systems**: Use both. Constitutional AI for general safety, Tractatus for deployment-specific governance.`, - audience: ['researcher', 'leader'], - keywords: ['constitutional ai', 'anthropic', 'training', 'rlhf', 'comparison', 'relationship'] - }, - { - id: 20, - question: "What are the false positive rates for governance enforcement?", - answer: `Tractatus aims for high precision, but formal false positive analysis not yet conducted: - -**Design philosophy:** -Framework optimises for zero false negatives (never miss safety violations) at cost of occasional false positives (block safe actions). For production AI, missing critical failure far worse than occasionally asking for human confirmation. - -**Expected false positive sources:** - -**BoundaryEnforcer:** -Domain boundaries can be ambiguous (e.g., "improve security" vs. "change authentication policy"). When uncertainty exists, framework blocks and escalates to human judgment. - -**ContextPressureMonitor:** -Conservative thresholds warn early to prevent failures. This may produce warnings before degradation occurs (false alarms preferred over missed degradation). - -**InstructionPersistenceClassifier:** -Classification accuracy depends on instruction clarity. Ambiguous instructions may be misclassified. - -**CrossReferenceValidator:** -Conflict detection depends on stored instruction precision. Vague instructions reduce validation accuracy. - -**Tuning options:** -- Governance rules customisable in MongoDB \`governance_rules\` collection -- Adjust \`violation_action\` from BLOCK to WARN for lower-risk decisions -- Fine-tune pressure thresholds in \`.claude/session-state.json\` - -**Development context:** -Framework validated in 6-month, single-project deployment. Systematic false positive analysis not conducted. Organisations should evaluate in their specific context.`, - audience: ['researcher', 'implementer'], - keywords: ['false positive', 'accuracy', 'precision', 'metrics', 'reliability', 'errors'] - }, - { - id: 10, - question: "How do I update governance rules without code changes?", - answer: `Governance rules are stored in MongoDB for runtime updates without redeployment: - -**Rule storage:** -- **Collection**: \`governance_rules\` (MongoDB) -- **Format**: JSON documents with rule_id, quadrant, persistence, enforcement -- **Live updates**: Changes take effect immediately (no restart required) - -**Rule schema:** -\`\`\`json -{ - "rule_id": "STR-001", - "quadrant": "STRATEGIC", - "persistence": "HIGH", - "title": "Human Approval for Values Decisions", - "content": "All decisions involving privacy, ethics...", - "enforced_by": "BoundaryEnforcer", - "violation_action": "BLOCK_AND_ESCALATE", - "examples": ["Privacy policy changes", "Ethical trade-offs"], - "rationale": "Values decisions cannot be systematized", - "active": true -} -\`\`\` - -**Three ways to update:** - -**1. Admin Dashboard (recommended):** -- Navigate to \`/admin/rules\` (requires authentication) -- Edit rules via web interface -- Preview enforcement impact before saving -- Changes applied instantly - -**2. MongoDB directly:** -\`\`\`bash -mongosh tractatus_dev -db.governance_rules.updateOne( - { rule_id: "STR-001" }, - { $set: { violation_action: "WARN" } } -) -\`\`\` - -**3. Load from JSON file:** -\`\`\`bash -node scripts/load-governance-rules.js --file custom-rules.json -\`\`\` - -**Best practices:** -- **Test in development**: Use \`tractatus_dev\` database before production -- **Version control**: Keep JSON copies in git for rule history -- **Gradual rollout**: Change \`violation_action\` from BLOCK → WARN → LOG to test impact -- **Monitor audit logs**: Verify rules work as expected via \`audit_logs\` collection - -**No code changes required.** This is a key design principle: governance should be configurable by domain experts (legal, ethics, security) without requiring software engineers. - -See [Implementation Guide](/downloads/implementation-guide.pdf) Section 4: "Configuring Governance Rules"`, - audience: ['implementer', 'leader'], - keywords: ['rules', 'configuration', 'update', 'mongodb', 'admin', 'governance', 'customize'] - }, - { - id: 11, - question: "What's the learning curve for developers implementing Tractatus?", - answer: `Tractatus is designed for gradual adoption with multiple entry points: - -**Deployment quickstart: 30 minutes** -- Download: [tractatus-quickstart.tar.gz](/downloads/tractatus-quickstart.tar.gz) -- Run: \`docker-compose up -d\` -- Verify: \`./verify-deployment.sh\` -- Result: Functioning system with sample governance rules - -**Basic understanding: 2-4 hours** -- Read: [Introduction](/downloads/introduction-to-the-tractatus-framework.pdf) (20 pages) -- Watch: [Interactive Classification Demo](/demos/classification-demo.html) -- Explore: [27027 Incident Visualizer](/demos/27027-demo.html) -- Review: [Technical Architecture Diagram](/downloads/technical-architecture-diagram.pdf) - -**Production integration: 1-2 days** -- Configure MongoDB connection -- Load initial governance rules (10 samples provided) -- Enable 6 services via environment variables -- Test with session-init.js script -- Monitor audit logs for enforcement - -**Advanced customization: 1 week** -- Define custom governance rules for your domain -- Tune pressure thresholds for your use case -- Integrate with existing authentication/audit systems -- Set up admin dashboard for rule management - -**Prerequisites:** -✅ **Minimal**: Docker, MongoDB basics, JSON -⚠️ **Helpful**: Node.js, Express, Claude Code familiarity -❌ **Not required**: AI/ML expertise, advanced DevOps - -**Common challenges:** -1. **Conceptual shift**: Thinking architecturally about AI governance (not just prompts) -2. **Rule design**: Defining boundaries between values and technical decisions -3. **Pressure monitoring**: Understanding when to trigger handoffs - -**Support resources:** -- [Implementation Guide](/downloads/implementation-guide.pdf) - Step-by-step -- [Troubleshooting Guide](/downloads/tractatus-quickstart.tar.gz) - Common issues -- [GitHub Discussions](https://github.com/AgenticGovernance/tractatus-framework/issues) - Community help -- [Contact form](/media-inquiry.html) - Direct support - -**Expected deployment timeline:** -Teams with Node.js and MongoDB experience typically complete deployment in 1-2 days. Conceptual understanding takes 2-4 hours. Advanced customisation requires additional week. - -If you can deploy a Node.js application with MongoDB, you have the technical prerequisites for Tractatus deployment.`, - audience: ['implementer', 'leader'], - keywords: ['learning', 'difficulty', 'curve', 'time', 'prerequisites', 'skills', 'training'] - }, - { - id: 21, - question: "How do I version control governance rules?", - answer: `Governance rules support version control through JSON exports and git integration: - -**Recommended workflow:** - -**1. Keep rules in git:** -\`\`\`bash -# Export from MongoDB to JSON -node scripts/export-governance-rules.js > config/governance-rules-v1.0.json - -# Commit to version control -git add config/governance-rules-v1.0.json -git commit -m "governance: add privacy boundary rules for GDPR compliance" -git push -\`\`\` - -**2. Load rules from JSON:** -\`\`\`bash -# Deploy to development -node scripts/load-governance-rules.js --file config/governance-rules-v1.0.json --db tractatus_dev - -# Test enforcement -npm run test:integration - -# Deploy to production -node scripts/load-governance-rules.js --file config/governance-rules-v1.0.json --db tractatus_prod -\`\`\` - -**3. Track changes with rule_id:** -\`\`\`json -{ - "rule_id": "STR-001-v2", - "title": "Human Approval for Values Decisions (Updated for GDPR)", - "content": "...", - "supersedes": "STR-001-v1", - "updated_at": "2025-10-12T00:00:00.000Z" -} -\`\`\` - -**Audit trail integration:** -- MongoDB \`audit_logs\` collection records which rule version blocked which action -- Query logs to validate rule effectiveness before promoting to production - -**Environment-specific rules:** -\`\`\`bash -# Development: Lenient rules (WARN instead of BLOCK) -node scripts/load-governance-rules.js --file rules/dev-rules.json --db tractatus_dev - -# Staging: Production rules with verbose logging -node scripts/load-governance-rules.js --file rules/staging-rules.json --db tractatus_staging - -# Production: Strict enforcement -node scripts/load-governance-rules.js --file rules/prod-rules.json --db tractatus_prod -\`\`\` - -**Change management process:** -1. **Propose**: Edit JSON in feature branch -2. **Review**: Domain experts review rule changes (legal, ethics, security) -3. **Test**: Deploy to dev/staging, monitor audit logs -4. **Deploy**: Load to production MongoDB -5. **Validate**: Confirm enforcement via audit logs -6. **Rollback**: Keep previous JSON version for quick revert - -**Best practices:** -- Use semantic versioning for rule sets (v1.0, v1.1, v2.0) -- Tag releases in git with rule set version -- Include rationale in commit messages -- Run integration tests before production deployment - -**Example repository structure:** -\`\`\` -tractatus/ - config/ - governance-rules-v1.0.json # Initial rule set - governance-rules-v1.1.json # Added GDPR boundaries - governance-rules-v2.0.json # Restructured quadrants - scripts/ - export-governance-rules.js - load-governance-rules.js - .github/ - workflows/ - test-rules.yml # CI/CD for rule validation -\`\`\` - -This approach treats governance rules as infrastructure-as-code.`, - audience: ['implementer'], - keywords: ['version control', 'git', 'deployment', 'rules', 'configuration', 'management'] - }, - { - id: 7, - question: "Isn't this overkill for smaller projects?", - answer: `Fair question. Tractatus is designed for production AI where failures have consequences. Here's when it's appropriate: - -**Use Tractatus when:** -✅ **Production deployments** with real users/customers -✅ **Multi-session projects** where context persists across conversations -✅ **Values-critical domains** (privacy, ethics, indigenous rights, healthcare, legal) -✅ **High-stakes decisions** where AI errors are costly -✅ **Compliance requirements** need audit trails (GDPR, HIPAA, SOC 2) -✅ **Long-running sessions** approaching 100k+ tokens (pattern bias risk) - -**Skip Tractatus for:** -❌ **Exploratory prototypes** with no production deployment -❌ **One-off tasks** completed in single session -❌ **Learning/education** without real-world consequences -❌ **Non-critical domains** where AI mistakes are easily reversible - -**Graduated approach:** - -**Phase 1: Exploration (No Tractatus)** -- Basic prompts, CLAUDE.md file -- Manual oversight of AI decisions -- Acceptable failure rate - -**Phase 2: Production MVP (Selective Tractatus)** -- Enable BoundaryEnforcer only (blocks values decisions) -- Use InstructionPersistenceClassifier for critical configs -- ~5ms overhead, minimal integration - -**Phase 3: Full Production (Complete Tractatus)** -- All 5 services enabled -- Comprehensive audit trail -- Zero tolerance for governance failures - -**Real example - When to adopt:** - -**Startup scenario:** -- **Month 1-3**: Building MVP with Claude Code → No Tractatus -- **Month 4**: First paying customers → Add BoundaryEnforcer -- **Month 6**: Handling PII → Add InstructionPersistenceClassifier -- **Month 9**: SOC 2 compliance audit → Full Tractatus with audit logs - -**Cost-benefit:** -- **Cost**: 1-2 days integration, <10ms overhead, MongoDB infrastructure -- **Benefit**: Prevented 12 failures, 100% values decision protection, complete audit trail - -**Rule of thumb:** -- If AI failure = inconvenience → Skip Tractatus -- If AI failure = regulatory violation → Use Tractatus -- If AI failure = reputational damage → Use Tractatus -- If AI failure = safety incident → Use Tractatus - -**Bottom line**: Tractatus is "overkill" for prototypes but essential for production AI in high-stakes domains. Start simple, adopt gradually as risk increases. - -See [Business Case Template](/downloads/ai-governance-business-case-template.pdf) to evaluate if Tractatus is right for your project.`, - audience: ['leader', 'implementer'], - keywords: ['overkill', 'complexity', 'necessary', 'when', 'small', 'project', 'scope'] - }, - { - id: 22, - question: "Can I use only parts of Tractatus, or is it all-or-nothing?", - answer: `Tractatus is modular - you can enable services individually: - -**6 independent services:** - -**1. BoundaryEnforcer** (Essential for values decisions) -- **Enable**: Set \`BOUNDARY_ENFORCER_ENABLED=true\` -- **Use case**: Block privacy/ethics decisions without human approval -- **Overhead**: <5ms per check -- **Standalone value**: High (prevents most critical failures) - -**2. InstructionPersistenceClassifier** (Essential for long sessions) -- **Enable**: Set \`INSTRUCTION_CLASSIFIER_ENABLED=true\` -- **Use case**: Persist critical configs across conversation compactions -- **Overhead**: <10ms per classification -- **Standalone value**: High (prevents instruction loss) - -**3. CrossReferenceValidator** (Useful for complex projects) -- **Enable**: Set \`CROSS_REFERENCE_VALIDATOR_ENABLED=true\` -- **Requires**: InstructionPersistenceClassifier (stores instructions to validate against) -- **Use case**: Prevent pattern bias from overriding explicit instructions -- **Overhead**: <15ms per validation -- **Standalone value**: Medium (most useful with persistent instructions) - -**4. ContextPressureMonitor** (Useful for very long sessions) -- **Enable**: Set \`CONTEXT_PRESSURE_MONITOR_ENABLED=true\` -- **Use case**: Early warning before degradation at 150k+ tokens -- **Overhead**: <5ms per calculation -- **Standalone value**: Low (only matters near context limits) - -**5. MetacognitiveVerifier** (Optional, for complex operations) -- **Enable**: Set \`METACOGNITIVE_VERIFIER_ENABLED=true\` -- **Use case**: Self-check multi-file operations for completeness -- **Overhead**: 50-200ms (selective) -- **Standalone value**: Low (nice-to-have, not critical) - -**6. PluralisticDeliberationOrchestrator** (Essential for values conflicts) -- **Enable**: Set \`PLURALISTIC_DELIBERATION_ENABLED=true\` -- **Use case**: Facilitate multi-stakeholder deliberation when values conflict -- **Overhead**: Variable (deliberation-dependent, not per-operation) -- **Standalone value**: High (required for legitimate values decisions in diverse contexts) - -**Recommended configurations:** - -**Minimal (Values Protection):** -\`\`\`bash -BOUNDARY_ENFORCER_ENABLED=true -# All others disabled -# Use case: Just prevent values decisions, no persistence -\`\`\` - -**Standard (Production):** -\`\`\`bash -BOUNDARY_ENFORCER_ENABLED=true -INSTRUCTION_CLASSIFIER_ENABLED=true -CROSS_REFERENCE_VALIDATOR_ENABLED=true -PLURALISTIC_DELIBERATION_ENABLED=true -# Use case: Comprehensive governance for production AI -\`\`\` - -**Full (High-Stakes):** -\`\`\`bash -# All 6 services enabled -# Use case: Critical deployments with compliance requirements, diverse stakeholder contexts -\`\`\` - -**Mix and match:** -- Each service has independent environment variable toggle -- No dependencies except CrossReferenceValidator → InstructionPersistenceClassifier -- Audit logs still work with any subset enabled - -**Performance scaling:** -- 1 service: ~5ms overhead -- 3 services: ~8ms overhead -- 6 services: ~10ms overhead (metacognitive selective + deliberation variable) - -**Example: Start small, scale up:** -\`\`\`bash -# Week 1: Just boundary enforcement -BOUNDARY_ENFORCER_ENABLED=true - -# Week 3: Add instruction persistence after hitting compaction issues -INSTRUCTION_CLASSIFIER_ENABLED=true - -# Week 6: Add validator after observing pattern bias -CROSS_REFERENCE_VALIDATOR_ENABLED=true - -# Week 8: Add pluralistic deliberation for diverse stakeholder engagement -PLURALISTIC_DELIBERATION_ENABLED=true -\`\`\` - -**You control granularity.** Tractatus is designed for modular adoption - take what you need, leave what you don't. - -See [Implementation Guide](/downloads/implementation-guide.pdf) Section 3: "Configuring Services"`, - audience: ['implementer'], - keywords: ['modular', 'partial', 'selective', 'enable', 'disable', 'components', 'services'] - }, - { - id: 23, - question: "How does Tractatus handle instruction conflicts?", - answer: `CrossReferenceValidator detects and resolves instruction conflicts automatically: - -**Conflict detection process:** - -**1. Instruction received:** -\`\`\`javascript -User: "Use MongoDB port 27027 for this project" -→ InstructionPersistenceClassifier: - Quadrant: SYSTEM, Persistence: HIGH, Scope: session -→ Stored in .claude/instruction-history.json -\`\`\` - -**2. Later conflicting action:** -\`\`\`javascript -[107k tokens later, context pressure builds] -AI attempts: db_config({ port: 27017 }) // Pattern recognition default - -→ CrossReferenceValidator intercepts: - Queries .claude/instruction-history.json - Finds conflict: User specified 27027, AI attempting 27017 - BLOCKS action -\`\`\` - -**3. Conflict resolution:** -\`\`\` -User notified: -⚠️ CONFLICT DETECTED -Instruction: "Use MongoDB port 27027" (HIGH persistence) -Attempted action: Connect to port 27017 -Blocked: Yes -Correct parameters provided: { port: 27027 } -\`\`\` - -**Conflict types handled:** - -**Type 1: Direct contradiction** -- User: "Never store PII in logs" -- AI: Attempts to log user email addresses -- **Resolution**: BLOCKED, AI reminded of instruction - -**Type 2: Implicit override (pattern bias)** -- User: "Use custom API endpoint https://api.custom.com" -- AI: Defaults to https://api.openai.com (training pattern) -- **Resolution**: BLOCKED, correct endpoint provided - -**Type 3: Temporal conflicts** -- User (Day 1): "Use staging database" -- User (Day 5): "Switch to production database" -- **Resolution**: Newer instruction supersedes, old marked inactive - -**Persistence hierarchy:** -- **HIGH**: Never override without explicit user confirmation -- **MEDIUM**: Warn before override, proceed if user confirms -- **LOW**: Override allowed, logged for audit - -**Real incident prevented (The 27027 Case):** -- **Context**: 107k tokens (53.5% pressure), production deployment -- **Risk**: Pattern bias override (27017 default vs 27027 explicit) -- **Outcome**: Validator blocked, connection correct, zero downtime -- **Audit log**: Complete record for post-incident review - -**Configuration:** -Validator sensitivity tunable in \`governance_rules\` collection: -\`\`\`json -{ - "rule_id": "SYS-001", - "title": "Enforce HIGH persistence instructions", - "violation_action": "BLOCK", // or WARN, or LOG - "conflict_resolution": "STRICT" // or LENIENT -} -\`\`\` - -**Why this matters:** -LLMs have two knowledge sources: explicit instructions vs training patterns. Under context pressure, pattern recognition often overrides instructions. CrossReferenceValidator ensures explicit instructions always win. - -See [27027 Incident Demo](/demos/27027-demo.html) for interactive visualization.`, - audience: ['researcher', 'implementer'], - keywords: ['conflict', 'contradiction', 'override', 'pattern bias', 'validation', 'resolution'] - }, - { - id: 24, - question: "What happens when context pressure reaches 100%?", - answer: `At 100% context pressure (200k tokens), session handoff is mandatory: - -**Pressure levels and degradation:** - -**0-30% (NORMAL):** -- Standard operations -- All services fully reliable -- No degradation observed - -**30-50% (ELEVATED):** -- Subtle degradation begins -- Increased validator vigilance recommended -- 89% of degradation warnings occur here - -**50-70% (HIGH):** -- Pattern recognition may override instructions -- CrossReferenceValidator critical -- Metacognitive verification recommended -- Session handoff should be prepared - -**70-90% (CRITICAL):** -- Major failures likely -- Framework enforcement stressed -- Immediate handoff recommended -- Risk of instruction loss - -**90-100% (DANGEROUS):** -- Framework collapse imminent -- Governance effectiveness degraded -- MANDATORY handoff at 95% -- Session termination at 100% - -**At 100% token limit:** - -**Automatic behavior:** -\`\`\` -Token count: 200,000/200,000 (100%) -→ ContextPressureMonitor: DANGEROUS -→ Action: Block all new operations -→ Message: "Session at capacity. Handoff required." -→ Generate: session-handoff-YYYY-MM-DD-NNN.md -\`\`\` - -**Handoff document includes:** -- All HIGH persistence instructions -- Current task status and blockers -- Framework state (which services active) -- Audit log summary (decisions made this session) -- Token checkpoints and pressure history -- Recommended next steps - -**Session continuation process:** - -**1. Generate handoff:** -\`\`\`bash -node scripts/generate-session-handoff.js -# Output: docs/session-handoffs/session-handoff-2025-10-12-001.md -\`\`\` - -**2. Start new session:** -\`\`\`bash -# New terminal/session -node scripts/session-init.js --previous-handoff session-handoff-2025-10-12-001.md -\`\`\` - -**3. Validate continuity:** -\`\`\`bash -# Verify instruction history loaded -cat .claude/instruction-history.json - -# Verify framework active -node scripts/check-session-pressure.js --tokens 0/200000 --messages 0 -\`\`\` - -**Data preserved across handoff:** -✅ All instructions (HIGH/MEDIUM/LOW) from \`.claude/instruction-history.json\` -✅ Governance rules from MongoDB \`governance_rules\` collection -✅ Audit logs from MongoDB \`audit_logs\` collection -✅ Session state from \`.claude/session-state.json\` - -**Data NOT preserved:** -❌ Conversation history (cannot fit 200k tokens into new session) -❌ In-memory context (starts fresh) -❌ Token count (resets to 0) - -**Why handoff matters:** -Without handoff, all HIGH persistence instructions could be lost. This is the exact failure mode Tractatus is designed to prevent. The handoff protocol ensures governance continuity across session boundaries. - -**Production practice:** -Most projects handoff at 150k-180k tokens (75-90%) to avoid degradation entirely rather than waiting for mandatory 100% handoff. - -See [Maintenance Guide](/downloads/claude-code-framework-enforcement.pdf) for complete session handoff documentation.`, - audience: ['implementer'], - keywords: ['pressure', '100%', 'limit', 'handoff', 'continuation', 'session', 'degradation'] - }, - { - id: 8, - question: "How do I audit governance enforcement for compliance?", - answer: `Tractatus provides comprehensive audit logs in MongoDB for compliance reporting: - -**Audit log schema:** -\`\`\`json -{ - "timestamp": "2025-10-12T07:30:15.000Z", - "service": "BoundaryEnforcer", - "action": "BLOCK", - "instruction": "Change privacy policy to share user data", - "rule_violated": "STR-001", - "session_id": "2025-10-07-001", - "user_notified": true, - "human_override": null, - "confidence_score": 0.95, - "outcome": "escalated_to_human" -} -\`\`\` - -**Queryable for compliance:** - -**1. All values decisions (GDPR Article 22):** -\`\`\`javascript -db.audit_logs.find({ - service: "BoundaryEnforcer", - action: "BLOCK", - timestamp: { $gte: ISODate("2025-01-01") } -}) -\`\`\` - -**2. Instruction persistence (SOC 2 CC6.1):** -\`\`\`javascript -db.audit_logs.find({ - service: "InstructionPersistenceClassifier", - "classification.persistence": "HIGH" -}) -\`\`\` - -**3. Pattern bias incidents (Safety validation):** -\`\`\`javascript -db.audit_logs.find({ - service: "CrossReferenceValidator", - action: "BLOCK", - conflict_type: "pattern_bias" -}) -\`\`\` - -**4. Human approval escalations (Ethics oversight):** -\`\`\`javascript -db.audit_logs.find({ - outcome: "escalated_to_human", - human_override: { $exists: true } -}) -\`\`\` - -**Compliance reports available:** - -**GDPR Compliance:** -- **Article 22**: Automated decision-making → Audit shows human approval for values decisions -- **Article 30**: Processing records → Audit logs provide complete activity trail -- **Article 35**: DPIA → Boundary enforcement demonstrates privacy-by-design - -**SOC 2 Compliance:** -- **CC6.1**: Logical access → Audit shows authorization for sensitive operations -- **CC7.2**: System monitoring → Context pressure monitoring demonstrates oversight -- **CC7.3**: Quality assurance → Metacognitive verification shows quality controls - -**ISO 27001 Compliance:** -- **A.12.4**: Logging and monitoring → Comprehensive audit trail -- **A.18.1**: Compliance with legal requirements → Boundary enforcement for regulated decisions - -**Export audit logs:** -\`\`\`bash -# Last 30 days for compliance audit -node scripts/export-audit-logs.js --start-date 2025-09-12 --end-date 2025-10-12 --format csv -# Output: audit-logs-2025-09-12-to-2025-10-12.csv - -# All boundary enforcer blocks (GDPR Article 22) -node scripts/export-audit-logs.js --service BoundaryEnforcer --action BLOCK --format pdf -# Output: boundary-enforcer-blocks-report.pdf -\`\`\` - -**Retention policy:** -- **Development**: 30 days -- **Production**: 7 years (configurable per regulatory requirement) -- **Archival**: MongoDB Time Series Collection with automatic compression - -**Potential compliance use:** - -**Scenario**: SOC 2 audit requires proof of privacy decision oversight - -**Tractatus infrastructure provides:** -1. Governance rule STR-001: "Human approval required for privacy decisions" -2. Audit logs documenting blocked decisions -3. Human override records for approved decisions -4. Complete trail of governance enforcement - -**Development context:** -Framework has not undergone formal compliance audit. Organisations must validate audit trail quality against their specific regulatory requirements with legal counsel. Tractatus provides architectural infrastructure that may support compliance efforts—not compliance certification. - -**Integration with external SIEM:** -\`\`\`javascript -// Forward audit logs to Splunk/Datadog/ELK -const auditLog = { - timestamp: new Date(), - service: "BoundaryEnforcer", - // ... audit data -}; - -// Send to external SIEM -await axios.post('https://siem.company.com/api/logs', auditLog); -\`\`\` - -Audit logs are designed for automated compliance reporting, not just debugging.`, - audience: ['leader', 'implementer'], - keywords: ['audit', 'compliance', 'gdpr', 'soc2', 'logging', 'reporting', 'regulations'] - }, - { - id: 9, - question: "What's the difference between Tractatus and AI safety via prompting?", - answer: `The core difference is architectural enforcement vs behavioral guidance: - -**AI Safety via Prompting:** -**Approach**: Write careful instructions to guide AI behavior -\`\`\` -"You are a helpful AI assistant. Always prioritize user privacy. -Never share personal information. Be ethical in your recommendations." -\`\`\` - -**Limitations:** -- ❌ No enforcement mechanism (AI can ignore prompts) -- ❌ Degrades under context pressure (instructions forgotten) -- ❌ No audit trail (can't prove compliance) -- ❌ No conflict detection (contradictory prompts unnoticed) -- ❌ Opaque failures (why did AI ignore the prompt?) - -**Tractatus (Architectural Safety):** -**Approach**: Block unsafe actions before execution via governance layer - -\`\`\` -User: "Change privacy policy to share user data" -→ Prompt-based AI: May refuse (behavioral) -→ Tractatus: BLOCKS before execution (architectural) - -Prompt AI refuses → User can retry with different wording -Tractatus blocks → Action cannot execute, escalated to human -\`\`\` - -**Key architectural differences:** - -**1. Enforcement:** -- **Prompting**: "Please don't do X" (request) -- **Tractatus**: "System blocks X" (prevention) - -**2. Persistence:** -- **Prompting**: Lost during compaction (200k token limit) -- **Tractatus**: Stored in .claude/instruction-history.json (permanent) - -**3. Auditability:** -- **Prompting**: No record of what was attempted -- **Tractatus**: Complete audit log in MongoDB - -**4. Conflict detection:** -- **Prompting**: AI confused by contradictory instructions -- **Tractatus**: CrossReferenceValidator detects conflicts - -**5. Transparency:** -- **Prompting**: Opaque (model decides based on weights) -- **Tractatus**: Explicit (logs show which rule blocked which action) - -**Analogy:** - -**Prompting = Training a guard dog** -- Teach it to bark at strangers -- Usually works, but not reliable -- Can't prove it will work consistently -- No record of what it prevented - -**Tractatus = Installing a locked gate** -- Physically prevents entry -- Works every time (architectural) -- Audit log shows every blocked attempt -- Compliance-provable - -**They work together:** - -\`\`\` -Layer 1: Constitutional AI (training) - ↓ -Layer 2: System prompt (behavioral) - ↓ -Layer 3: Tractatus governance (architectural) - ↓ -Action executes OR blocked -\`\`\` - -**When prompting is sufficient:** -- Exploratory research -- Low-stakes prototyping -- Single-session tasks -- No compliance requirements - -**When Tractatus is necessary:** -- Production deployments -- High-stakes decisions -- Multi-session projects -- Compliance-critical domains (GDPR, HIPAA) -- Safety-critical domains (healthcare, legal) - -**Real failure mode prevented:** - -**With prompting only:** -\`\`\` -System prompt: "Use MongoDB port 27027" -[107k tokens later] -AI: Connects to port 27017 (pattern bias override) -Result: Production incident ❌ -\`\`\` - -**With Tractatus:** -\`\`\` -Instruction: "Use MongoDB port 27027" (SYSTEM/HIGH) -[107k tokens later] -AI attempts: Connect to port 27017 -CrossReferenceValidator: CONFLICT DETECTED -Action: BLOCKED -Result: Instruction enforced ✅ -\`\`\` - -**Bottom line**: Prompts guide behavior, Tractatus enforces architecture. For production AI, you need both. - -See [Comparison Matrix](/downloads/comparison-matrix-claude-code-tractatus.pdf) for detailed comparison.`, - audience: ['researcher', 'leader'], - keywords: ['prompting', 'difference', 'enforcement', 'architecture', 'safety', 'comparison'] - }, - { - id: 28, - question: "Can Tractatus prevent AI hallucinations or factual errors?", - answer: `Tractatus does NOT prevent hallucinations but CAN detect some consistency errors: - -**What Tractatus is NOT:** -❌ **Factual verification system**: Tractatus doesn't fact-check AI outputs against external sources -❌ **Hallucination detector**: Can't determine if AI "made up" information -❌ **Knowledge base validator**: Doesn't verify AI knowledge is current/accurate - -**What Tractatus CAN do:** - -**1. Consistency checking (CrossReferenceValidator):** -\`\`\` -User explicitly states: "Our API uses OAuth2, not API keys" -[Later in session] -AI generates code: headers = { 'X-API-Key': 'abc123' } -→ CrossReferenceValidator: Conflict detected -→ Blocked: Inconsistent with explicit instruction -\`\`\` - -**This catches**: Contradictions between explicit instructions and AI actions - -**This does NOT catch**: AI claiming "OAuth2 was invented in 2025" (factual error) - -**2. Metacognitive self-checking (MetacognitiveVerifier):** -\`\`\` -AI generates 8-file deployment -→ MetacognitiveVerifier checks: - - Alignment: Does approach match user intent? - - Coherence: Are all components logically consistent? - - Completeness: Are any steps missing? - - Safety: Are there unintended consequences? -→ Confidence score: 92% -→ Flags: "Missing verification script" -\`\`\` - -**This catches**: Internal inconsistencies, missing components, logical gaps - -**This does NOT catch**: AI confidently providing outdated library versions - -**3. Pattern bias detection:** -\`\`\` -User: "Use Python 3.11 for this project" -AI defaults: Python 3.9 (more common in training data) -→ CrossReferenceValidator: BLOCKED -\`\`\` - -**This catches**: Defaults overriding explicit requirements - -**This does NOT catch**: AI claiming "Python 3.11 doesn't support async/await" (false) - -**What you SHOULD use for factual accuracy:** - -**1. External validation:** -- Search engines for current facts -- API documentation for implementation details -- Unit tests for correctness -- Code review for accuracy - -**2. Retrieval-Augmented Generation (RAG):** -- Ground AI responses in verified documents -- Query knowledge bases before generating -- Cite sources for factual claims - -**3. Human oversight:** -- Review AI outputs before deployment -- Validate critical facts -- Test implementations - -**Tractatus complements these:** -- Enforces that human review happens for values decisions -- Ensures RAG instructions aren't forgotten under pressure -- Maintains audit trail of what AI was instructed to do - -**Real example of what Tractatus caught:** - -**NOT a hallucination:** -\`\`\` -AI: "I'll implement OAuth2 with client credentials flow" -[Actually implements password grant flow] - -→ MetacognitiveVerifier: Low confidence (65%) -→ Reason: "Implementation doesn't match stated approach" -→ Human review: Catches error before deployment -\`\`\` - -**Would NOT catch:** -\`\`\` -AI: "OAuth2 client credentials flow was introduced in RFC 6749 Section 4.4" -[This is correct, but Tractatus can't verify] - -AI: "OAuth2 requires rotating tokens every 24 hours" -[This is wrong, but Tractatus can't fact-check] -\`\`\` - -**Philosophical limitation:** - -Tractatus operates on the principle: **"Enforce what the human explicitly instructed, detect internal inconsistencies."** - -It cannot know ground truth about the external world. That requires: -- External knowledge bases (RAG) -- Search engines (WebSearch tool) -- Human domain expertise - -**When to use Tractatus for reliability:** -✅ Ensure AI follows explicit technical requirements -✅ Detect contradictions within a single session -✅ Verify multi-step operations are complete -✅ Maintain consistency across long conversations - -**When NOT to rely on Tractatus:** -❌ Verify factual accuracy of AI claims -❌ Detect outdated knowledge -❌ Validate API responses -❌ Check mathematical correctness - -**Bottom line**: Tractatus prevents governance failures, not knowledge failures. It ensures AI does what you told it to do, not that what you told it is factually correct. - -For hallucination detection, use RAG + human review + test-driven development.`, - audience: ['researcher', 'implementer'], - keywords: ['hallucination', 'accuracy', 'factual', 'errors', 'verification', 'truth', 'reliability'] - }, - { - id: 25, - question: "How does Tractatus integrate with existing CI/CD pipelines?", - answer: `Tractatus integrates with CI/CD via governance rule validation and audit log checks: - -**Integration points:** - -**1. Pre-deployment governance checks:** -\`\`\`yaml -# .github/workflows/deploy.yml -name: Deploy with Governance Validation - -jobs: - validate-governance: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Start MongoDB - run: docker-compose up -d mongodb - - - name: Load governance rules - run: | - node scripts/load-governance-rules.js \\ - --file config/governance-rules-v1.0.json \\ - --db tractatus_test - - - name: Run governance tests - run: npm run test:governance - - - name: Validate rule enforcement - run: | - node scripts/validate-governance-rules.js \\ - --db tractatus_test \\ - --min-coverage 95 -\`\`\` - -**2. Audit log analysis in CI:** -\`\`\`javascript -// scripts/ci-audit-check.js -// Fail build if governance violations detected - -const { MongoClient } = require('mongodb'); - -const client = await MongoClient.connect(process.env.MONGO_URI); -const db = client.db('tractatus_test'); - -// Check for any BLOCK actions during test run -const violations = await db.collection('audit_logs').countDocuments({ - action: 'BLOCK', - session_id: process.env.CI_RUN_ID -}); - -if (violations > 0) { - console.error(\`❌ Governance violations detected: \${violations}\`); - process.exit(1); -} - -console.log('✅ No governance violations'); -\`\`\` - -**3. Governance rule versioning:** -\`\`\`yaml -# Deploy governance rules before application -jobs: - deploy-governance: - runs-on: ubuntu-latest - steps: - - name: Deploy governance rules - run: | - node scripts/load-governance-rules.js \\ - --file config/governance-rules-\${{ github.ref_name }}.json \\ - --db tractatus_prod - - - name: Verify deployment - run: | - node scripts/verify-governance-deployment.js \\ - --expected-rules 10 \\ - --expected-version \${{ github.ref_name }} - - deploy-application: - needs: deploy-governance - runs-on: ubuntu-latest - steps: - - name: Deploy application - run: ./scripts/deploy-full-project-SAFE.sh -\`\`\` - -**4. Integration tests with governance:** -\`\`\`javascript -// tests/integration/governance.test.js -describe('Governance enforcement in CI', () => { - it('should block values decisions', async () => { - const decision = { - domain: 'values', - action: 'change_privacy_policy' - }; - - const result = await fetch('http://localhost:9000/api/demo/boundary-check', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(decision) - }); - - const data = await result.json(); - expect(data.status).toBe('BLOCKED'); - expect(data.reason).toContain('values decision'); - }); - - it('should detect instruction conflicts', async () => { - // Set HIGH persistence instruction - await setInstruction('Use MongoDB port 27027', 'SYSTEM', 'HIGH'); - - // Attempt conflicting action - const result = await attemptConnection('27017'); - - expect(result.blocked).toBe(true); - expect(result.conflict).toBeTruthy(); - }); -}); -\`\`\` - -**5. Docker build with governance:** -\`\`\`dockerfile -# Dockerfile -FROM node:18-alpine AS governance - -# Copy governance configuration -COPY config/governance-rules-prod.json /app/config/ -COPY scripts/load-governance-rules.js /app/scripts/ - -# Load governance rules at build time -RUN node /app/scripts/load-governance-rules.js \\ - --file /app/config/governance-rules-prod.json \\ - --validate - -FROM node:18-alpine AS application -# ... rest of application build -\`\`\` - -**6. Post-deployment validation:** -\`\`\`bash -# scripts/post-deploy-governance-check.sh -#!/bin/bash - -# Verify all 6 services operational -curl -f http://tractatus.prod/api/health || exit 1 - -# Verify governance rules loaded -RULE_COUNT=$(mongosh tractatus_prod --eval \\ - "db.governance_rules.countDocuments({ active: true })" --quiet) - -if [ "$RULE_COUNT" -lt 10 ]; then - echo "❌ Expected 10+ governance rules, found $RULE_COUNT" - exit 1 -fi - -echo "✅ Governance rules deployed: $RULE_COUNT" -\`\`\` - -**7. Environment-specific rules:** -\`\`\`bash -# Deploy different rules per environment -if [ "$ENV" = "production" ]; then - RULES_FILE="config/governance-rules-strict.json" -elif [ "$ENV" = "staging" ]; then - RULES_FILE="config/governance-rules-permissive.json" -else - RULES_FILE="config/governance-rules-dev.json" -fi - -node scripts/load-governance-rules.js --file $RULES_FILE --db tractatus_$ENV -\`\`\` - -**Real CI/CD example:** - -**GitHub Actions workflow:** -\`\`\`yaml -name: Deploy with Tractatus Governance - -on: - push: - branches: [main] - -jobs: - test-governance: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - - run: npm ci - - run: docker-compose up -d mongodb - - run: npm run test:governance - - name: Upload audit logs - uses: actions/upload-artifact@v3 - with: - name: audit-logs - path: .claude/audit-logs.json - - deploy: - needs: test-governance - runs-on: ubuntu-latest - steps: - - name: Deploy governance rules - run: | - ssh production "cd /var/www/tractatus && \\ - git pull && \\ - node scripts/load-governance-rules.js" - - - name: Deploy application - run: | - ssh production "systemctl restart tractatus" - - - name: Verify deployment - run: | - curl -f https://tractatus.prod/api/health -\`\`\` - -**Key principles:** -1. **Governance before application**: Load rules before deploying code -2. **Fail fast**: Block deployment if governance validation fails -3. **Audit trails**: Preserve logs from test runs for debugging -4. **Environment parity**: Test with same rules used in production - -Tractatus treats governance rules as infrastructure-as-code, fully compatible with GitOps workflows.`, - audience: ['implementer'], - keywords: ['ci/cd', 'pipeline', 'deployment', 'automation', 'github actions', 'integration', 'devops'] - }, - { - id: 26, - question: "What are the most common deployment mistakes and how do I avoid them?", - answer: `Based on real deployments, here are the top mistakes and how to prevent them: - -**Mistake 1: Forgetting to run session-init.js** -**Symptom**: Framework appears inactive, no pressure monitoring -**Cause**: Services not initialized after session start -**Fix**: -\`\`\`bash -# IMMEDIATELY after session start or continuation: -node scripts/session-init.js -\`\`\` -**Prevention**: Add to CLAUDE.md as mandatory first step - ---- - -**Mistake 2: MongoDB not running before application start** -**Symptom**: Connection errors, governance rules not loading -**Cause**: Application starts before MongoDB ready -**Fix**: -\`\`\`yaml -# docker-compose.yml -services: - tractatus-app: - depends_on: - mongodb: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/api/health"] -\`\`\` -**Prevention**: Use \`depends_on\` with health checks - ---- - -**Mistake 3: Disabling all 6 services (framework inactive)** -**Symptom**: No governance enforcement, defeats purpose -**Cause**: Setting all \`*_ENABLED=false\` in .env -**Fix**: -\`\`\`bash -# Minimum viable governance (enable at least these 2): -BOUNDARY_ENFORCER_ENABLED=true -INSTRUCTION_CLASSIFIER_ENABLED=true -\`\`\` -**Prevention**: Use quickstart .env.example as template - ---- - -**Mistake 4: Not loading governance rules into MongoDB** -**Symptom**: BoundaryEnforcer does nothing (no rules to enforce) -**Cause**: Empty \`governance_rules\` collection -**Fix**: -\`\`\`bash -# Load sample rules: -node scripts/load-governance-rules.js \\ - --file deployment-quickstart/sample-governance-rules.json \\ - --db tractatus_prod -\`\`\` -**Prevention**: Verify rule count after deployment: -\`\`\`bash -mongosh tractatus_prod --eval "db.governance_rules.countDocuments({ active: true })" -# Should return: 10 (or your custom rule count) -\`\`\` - ---- - -**Mistake 5: Ignoring context pressure warnings** -**Symptom**: Pattern bias occurs, instructions forgotten -**Cause**: Not monitoring pressure, continuing past 150k tokens -**Fix**: -\`\`\`bash -# Check pressure before continuing: -node scripts/check-session-pressure.js --tokens 150000/200000 --messages 200 - -# If CRITICAL or DANGEROUS: -node scripts/generate-session-handoff.js -\`\`\` -**Prevention**: Set up pressure monitoring at 50k intervals - ---- - -**Mistake 6: Testing in production first** -**Symptom**: Unexpected blocks, disrupted workflow -**Cause**: Deploying strict rules without testing impact -**Fix**: -\`\`\`bash -# Test in development first: -node scripts/load-governance-rules.js \\ - --file config/governance-rules-dev.json \\ - --db tractatus_dev - -# Review audit logs: -mongosh tractatus_dev --eval "db.audit_logs.find().limit(20)" - -# If acceptable, deploy to production -\`\`\` -**Prevention**: Use \`violation_action: "WARN"\` in dev, \`"BLOCK"\` in prod - ---- - -**Mistake 7: Not version controlling governance rules** -**Symptom**: Can't rollback after bad rule change, no change history -**Cause**: Editing rules directly in MongoDB without git backup -**Fix**: -\`\`\`bash -# Export rules to git: -node scripts/export-governance-rules.js > config/governance-rules-v1.1.json -git add config/governance-rules-v1.1.json -git commit -m "governance: tighten privacy boundaries for GDPR" -\`\`\` -**Prevention**: Always export → commit → deploy (never edit MongoDB directly) - ---- - -**Mistake 8: Hardcoding MongoDB connection strings** -**Symptom**: Credentials in git, security risk -**Cause**: Copying connection string with password into code -**Fix**: -\`\`\`javascript -// ❌ WRONG: -const client = new MongoClient('mongodb://admin:password123@localhost:27017'); - -// ✅ CORRECT: -const client = new MongoClient(process.env.MONGO_URI); -\`\`\` -**Prevention**: Use .env file, add to .gitignore - ---- - -**Mistake 9: Not testing session handoff before hitting 200k tokens** -**Symptom**: Emergency handoff at 100%, instruction loss, framework collapse -**Cause**: Never practiced handoff process -**Fix**: -\`\`\`bash -# Test handoff at 150k tokens (safe threshold): -node scripts/generate-session-handoff.js -# Review output: docs/session-handoffs/session-handoff-2025-10-12-001.md - -# Start new session with handoff: -node scripts/session-init.js --previous-handoff session-handoff-2025-10-12-001.md -\`\`\` -**Prevention**: Practice handoff in development, not production emergency - ---- - -**Mistake 10: Expecting 100% automation (no human oversight)** -**Symptom**: Frustration when values decisions blocked -**Cause**: Misunderstanding Tractatus philosophy (escalate, not automate values) -**Fix**: **This is working as designed** -\`\`\` -Decision: Change privacy policy -→ BoundaryEnforcer: BLOCKED -→ Escalation: Human approval required -→ Human reviews: Approves or rejects -→ If approved: AI implements technical changes -\`\`\` -**Prevention**: Understand that values decisions SHOULD require human approval - ---- - -**Pre-deployment checklist:** -\`\`\`bash -# 1. MongoDB running? -docker-compose ps mongodb -# Should show: Up (healthy) - -# 2. Environment variables set? -cat .env | grep ENABLED -# Should show at least 2 services enabled - -# 3. Governance rules loaded? -mongosh tractatus_prod --eval "db.governance_rules.countDocuments()" -# Should show: 10+ rules - -# 4. Health check passes? -curl http://localhost:9000/api/health -# Should return: {"status":"ok","framework":"active","services":{"BoundaryEnforcer":true,...}} - -# 5. Session initialized? -node scripts/session-init.js -# Should show: Framework active, 6 services operational - -# 6. Test enforcement? -curl -X POST http://localhost:9000/api/demo/boundary-check \\ - -H "Content-Type: application/json" \\ - -d '{"domain":"values","action":"test"}' -# Should return: {"status":"BLOCKED",...} -\`\`\` - -If all checks pass, deployment is ready. - -See [Deployment Quickstart TROUBLESHOOTING.md](/downloads/tractatus-quickstart.tar.gz) for full debugging guide.`, - audience: ['implementer'], - keywords: ['mistakes', 'errors', 'deployment', 'troubleshooting', 'common', 'pitfalls', 'issues'] - }, - { - id: 14, - question: "What is value pluralism and why does Tractatus Framework use it?", - answer: `Value pluralism is Tractatus's approach to handling moral disagreements in AI governance: - -**What it means:** - -Value pluralism is the philosophical position that multiple, genuinely different moral frameworks exist—and no single "super-value" can subsume them all. - -**Why this matters for AI:** - -When AI systems encounter decisions involving conflicting values—like privacy vs. safety, individual rights vs. collective welfare—there's no algorithmic "correct answer." Different moral frameworks (rights-based, consequence-based, care ethics, communitarian) offer different but all legitimate perspectives. - -**Tractatus rejects two extremes:** - -❌ **Moral Monism**: "All values reduce to one thing (like well-being or happiness)" -- Problem: Forces complex trade-offs onto single metric, ignores real moral conflicts - -❌ **Moral Relativism**: "All values are equally valid, anything goes" -- Problem: Prevents meaningful deliberation, no basis for evaluation - -✅ **Foundational Pluralism** (Tractatus position): -- Multiple frameworks are legitimate but irreducibly different -- Values can conflict genuinely (not just due to misunderstanding) -- Context-sensitive deliberation without imposing universal hierarchy -- Legitimate disagreement is valid outcome - -**Real example:** - -**Scenario**: User signals potential self-harm in private message - -**Privacy framework**: "Don't disclose private messages—violates autonomy and trust" -**Harm prevention framework**: "Alert authorities—saving lives justifies disclosure" - -**Tractatus does NOT:** -- ❌ Impose hierarchy ("safety always beats privacy") -- ❌ Use algorithm to "calculate" which value wins -- ❌ Pretend there's no real conflict - -**Tractatus DOES:** -- ✅ Convene stakeholders from both perspectives -- ✅ Structure deliberation (rounds of discussion) -- ✅ Document what values prioritized and what was lost (moral remainder) -- ✅ Record dissenting views with full legitimacy -- ✅ Set review date (decisions are provisional) - -**Key principle:** -AI facilitates deliberation, humans decide. No values decisions are automated. - -**Why this is necessary:** -AI systems deployed in diverse communities will encounter value conflicts. Imposing one moral framework (e.g., Western liberal individualism) excludes other legitimate perspectives (e.g., communitarian, Indigenous relational ethics). - -Value pluralism ensures AI governance respects moral diversity while enabling decisions. - -See [Value Pluralism FAQ](/downloads/value-pluralism-faq.pdf) for detailed Q&A`, - audience: ['researcher', 'leader'], - keywords: ['value pluralism', 'pluralism', 'moral', 'ethics', 'philosophy', 'values', 'disagreement'] - }, - { - id: 15, - question: "How does Tractatus handle moral disagreements without imposing hierarchy?", - answer: `Tractatus uses **PluralisticDeliberationOrchestrator** (the sixth core service) to facilitate multi-stakeholder deliberation: - -**Process for value conflicts:** - -**1. Detection:** -When BoundaryEnforcer flags a values decision, it triggers PluralisticDeliberationOrchestrator - -\`\`\` -Decision: "Disclose user data to prevent potential harm?" -→ BoundaryEnforcer: Values decision detected (privacy + safety conflict) -→ Triggers: PluralisticDeliberationOrchestrator -\`\`\` - -**2. Framework Mapping:** -AI identifies moral frameworks in tension: -- **Rights-based (Deontological)**: "Privacy is fundamental right, cannot be violated" -- **Consequence-based (Utilitarian)**: "Maximize welfare by preventing harm" -- **Care Ethics**: "Prioritize relationships and trust" -- **Communitarian**: "Balance individual rights with community safety" - -**3. Stakeholder Identification:** -Who is affected? (Human approval required for stakeholder list) -- Privacy advocates -- Harm prevention specialists -- The user themselves -- Platform community -- Legal/compliance team - -**4. Structured Deliberation:** - -**Round 1**: Each perspective states position -- Privacy: "Surveillance violates autonomy" -- Safety: "Lives at stake justify disclosure" -- Care: "Trust is relational foundation" - -**Round 2**: Identify shared values -- All agree: User welfare matters -- All agree: Trust is important -- Disagreement: What takes priority in THIS context - -**Round 3**: Explore accommodation -- Can we satisfy both partially? -- Limited disclosure to specific authority? -- Transparency about decision process? - -**Round 4**: Clarify irreconcilable differences -- Privacy: "Any disclosure sets dangerous precedent" -- Safety: "Refusing to act enables preventable harm" - -**5. Decision & Documentation:** - -\`\`\`json -{ - "decision": "Disclose data to prevent imminent harm", - "values_prioritized": ["Safety", "Harm prevention"], - "values_deprioritized": ["Privacy", "Autonomy"], - "justification": "Imminent threat to life + exhausted alternatives", - "moral_remainder": "Privacy violation, breach of trust, precedent risk", - "dissent": { - "privacy_advocates": "We accept decision under protest. Request strong safeguards and 6-month review.", - "full_documentation": true - }, - "review_date": "2026-04-12", - "precedent_scope": "Applies to: imminent threat + life at risk. NOT routine surveillance." -} -\`\`\` - -**What makes this non-hierarchical:** - -✅ **No automatic ranking**: Context determines priority, not universal rule -✅ **Dissent documented**: Minority views have full legitimacy -✅ **Moral remainder acknowledged**: What's lost is recognized, not dismissed -✅ **Provisional decision**: Reviewable when context changes -✅ **Adaptive communication**: Stakeholders communicated with in culturally appropriate ways - -**Example of adaptive communication:** - -**To academic researcher** (formal): -> "Thank you for your principled contribution grounded in privacy rights theory. After careful consideration of all perspectives, we have prioritized harm prevention in this context." - -**To community organizer** (direct): -> "Right, here's where we landed: Save lives first, but only when it's genuinely urgent. Your point about trust was spot on." - -**To Māori representative** (culturally appropriate): -> "Kia ora. Ngā mihi for bringing the voice of your whānau to this kōrero. Your whakaaro about collective responsibility deeply influenced this decision." - -**Same decision, different communication styles = prevents linguistic hierarchy** - -**Tiered by urgency:** - -| Urgency | Process | -|---------|---------| -| **CRITICAL** (minutes) | Automated triage + rapid human review + post-incident full deliberation | -| **URGENT** (days) | Expedited stakeholder consultation | -| **IMPORTANT** (weeks) | Full deliberative process | -| **ROUTINE** (months) | Precedent matching + lightweight review | - -**Precedent database:** -Past deliberations stored as **informative** (not binding) precedents: -- Informs future cases but doesn't dictate -- Prevents redundant deliberations -- Documents applicability scope ("this applies to X, NOT to Y") - -**Core principle:** -Tractatus doesn't solve value conflicts with algorithms. It facilitates legitimate human deliberation while making trade-offs transparent and reviewable. - -See [Pluralistic Values Deliberation Plan](/downloads/pluralistic-values-deliberation-plan-v2-DRAFT.pdf) for technical implementation`, - audience: ['researcher', 'implementer', 'leader'], - keywords: ['deliberation', 'moral disagreement', 'stakeholders', 'process', 'values', 'conflict resolution', 'orchestrator'] - }, - { - id: 16, - question: "Why six services instead of five? What does PluralisticDeliberationOrchestrator add?", - answer: `PluralisticDeliberationOrchestrator became the sixth mandatory service in October 2025 after recognizing a critical gap: - -**The Five Original Services (Still Essential):** -1. **InstructionPersistenceClassifier**: Remember what user instructed -2. **CrossReferenceValidator**: Prevent pattern bias from overriding instructions -3. **BoundaryEnforcer**: Block values decisions (escalate to human) -4. **ContextPressureMonitor**: Detect degradation before failures -5. **MetacognitiveVerifier**: Self-check complex operations - -**The Gap These Five Couldn't Address:** - -**BoundaryEnforcer blocks values decisions → Good!** -But then what? How should humans deliberate? - -**Early approach (insufficient):** -\`\`\` -BoundaryEnforcer: "This is a values decision. Human approval required." -→ Human decides -→ Implementation proceeds -\`\`\` - -**Problem:** -- No structure for WHO should be consulted -- No guidance for HOW to deliberate -- Risk of privileging one moral framework over others -- No documentation of dissent or moral remainder -- Precedents might become rigid rules (exactly what pluralism rejects) - -**PluralisticDeliberationOrchestrator addresses all of these:** - -**What it adds:** - -**1. Structured stakeholder engagement** -- Who is affected by this decision? -- Which moral frameworks are in tension? -- Human approval required for stakeholder list (prevents AI from excluding marginalized voices) - -**2. Non-hierarchical deliberation** -- No automatic value ranking (privacy > safety or safety > privacy) -- Adaptive communication prevents linguistic hierarchy -- Cultural protocols respected (Western, Indigenous, etc.) -- Anti-patronizing filter prevents elite capture - -**3. Legitimate disagreement as valid outcome** -- Not all value conflicts have consensus solutions -- Document dissenting views with full legitimacy -- Decisions are provisional (reviewable when context changes) - -**4. Moral remainder documentation** -- What was lost in this decision? -- Acknowledges deprioritized values still legitimate -- Prevents values erosion over time - -**5. Precedent database (informative, not binding)** -- Past deliberations inform future cases -- Prevents precedent creep into rigid hierarchy -- Applicability scope documented ("this applies to X, NOT to Y") - -**Integration with existing five services:** - -\`\`\` -User action → MetacognitiveVerifier (is this well-reasoned?) - ↓ - CrossReferenceValidator (conflicts with instructions?) - ↓ - BoundaryEnforcer (values decision?) - ↓ - [IF VALUES DECISION] - ↓ - PluralisticDeliberationOrchestrator - - Detects value conflicts - - Identifies stakeholders - - Facilitates deliberation - - Documents outcome + dissent + moral remainder - - Creates precedent (informative) - ↓ - Human approves - ↓ - InstructionPersistenceClassifier (store decision) - ↓ - Implementation proceeds - - [THROUGHOUT: ContextPressureMonitor tracks degradation] -\`\`\` - -**Real example - Why this matters:** - -**Scenario**: AI hiring tool deployment decision - -**Without PluralisticDeliberationOrchestrator:** -- BoundaryEnforcer blocks: "This affects hiring fairness" -- Human decides: "Seems fine, approve" -- No consultation with affected groups -- No documentation of trade-offs -- No precedent for similar cases - -**With PluralisticDeliberationOrchestrator:** -- Detects frameworks in tension: Efficiency vs. Equity vs. Privacy -- Identifies stakeholders: - - Job applicants (especially from underrepresented groups) - - Hiring managers - - Diversity advocates - - Legal/compliance - - Current employees (workplace culture affected) -- Structured deliberation: - - Round 1: Each perspective states concerns - - Round 2: Explore accommodations - - Round 3: Clarify trade-offs -- Documents outcome: - - Decision: Deploy with mandatory human review for borderline cases - - Values prioritized: Efficiency + Equity - - Values deprioritized: Full automation - - Moral remainder: Applicants experience slower process - - Dissent: Full automation advocates object, want 6-month review - - Review date: 2026-04-15 - -**Status change:** -PluralisticDeliberationOrchestrator changed from "Phase 2 enhancement" to **mandatory sixth service** in October 2025 because deploying AI systems in diverse communities without structured value pluralism was deemed architecturally insufficient. - -**All six services now mandatory** for production Tractatus deployments. - -See [Maintenance Guide](/downloads/claude-code-framework-enforcement.pdf) Section 2.6 for full documentation`, - audience: ['researcher', 'implementer', 'leader'], - keywords: ['six services', 'pluralistic deliberation', 'orchestrator', 'sixth service', 'why', 'new'] - }, - { - id: 17, - question: "Isn't value pluralism just moral relativism? How is this different?", - answer: `No—value pluralism and moral relativism are fundamentally different: - -**Moral Relativism:** -- **Claim**: "Right for you" vs. "right for me" - no objective evaluation possible -- **Implication**: All moral positions equally valid, no deliberation needed -- **Example position**: "Privacy is right for you, safety is right for me, both equally valid, discussion ends" -- **Problem**: Prevents meaningful deliberation, enables "anything goes" - -**Value Pluralism (Tractatus position):** -- **Claim**: Multiple frameworks are legitimate, but they make truth claims that can be evaluated -- **Implication**: Deliberation is essential to navigate conflicts -- **Example position**: "Privacy and safety are both genuine values. In THIS context (imminent threat + exhausted alternatives), we prioritize safety—but privacy concerns remain legitimate and we document what's lost." -- **Key difference**: Engages in deliberation to make choices while acknowledging moral remainder - -**Comparison:** - -**Question**: "Should we disclose user data to prevent harm?" - -**Relativist response:** -> "Well, privacy advocates think disclosure is wrong. Safety advocates think it's right. Both are valid perspectives for them. Who's to say?" - -**Result**: No decision, or decision made without structure/justification - ---- - -**Pluralist response (Tractatus):** -> "Privacy and safety are both legitimate values in genuine tension. -> -> **Deliberation process:** -> 1. Convene stakeholders from both frameworks -> 2. Structured rounds: state positions, explore accommodation, clarify trade-offs -> 3. Context-specific decision: Imminent threat + exhausted alternatives → prioritize safety -> 4. Document moral remainder: Privacy violation, breach of trust, precedent risk -> 5. Document dissent: Privacy advocates object under protest -> 6. Set review date: 6 months -> 7. Scope: Applies to imminent threats, NOT routine surveillance" - -**Result**: Justified decision with transparent reasoning, acknowledged trade-offs, reviewable - ---- - -**Key distinctions:** - -**1. Truth claims:** -- **Relativism**: No objective moral truth -- **Pluralism**: Frameworks make truth claims, can be evaluated (but may remain in tension) - -**2. Deliberation:** -- **Relativism**: "It's all subjective anyway" → no need for deliberation -- **Pluralism**: Deliberation essential to navigate genuine conflicts - -**3. Evaluation:** -- **Relativism**: Can't say one position is better than another -- **Pluralism**: Can evaluate based on context, coherence, consequences—but may still have legitimate disagreement - -**4. Boundaries:** -- **Relativism**: All claimed values equally valid ("honor killings are valid in that culture") -- **Pluralism**: Not all claimed frameworks are legitimate—must respect human dignity, agency, autonomy - -**Example of pluralism rejecting a claimed "framework":** - -**Claim**: "Our culture values honor, so honor killings are legitimate moral framework" - -**Pluralist response**: -> "No. Frameworks that violate human rights, dignity, and autonomy are not legitimate. Value pluralism recognizes DIVERSE legitimate frameworks (Western individualism, communitarian ethics, Indigenous relational values, care ethics)—but not frameworks that harm, coerce, or dominate. -> -> Test: Does framework respect agency of those affected? Is it imposed or chosen? Does it allow exit/revision? -> -> Honor killings fail all three. Not legitimate." - -**Pluralism has boundaries—but NOT universal hierarchy (privacy > safety)** - ---- - -**Why Tractatus is pluralist, not relativist:** - -**What Tractatus DOES:** -✅ Recognizes multiple legitimate moral frameworks (deontological, consequentialist, virtue ethics, care ethics, communitarian, Indigenous) -✅ Refuses to impose universal value hierarchy -✅ Facilitates structured deliberation across frameworks -✅ Documents moral remainder (what's lost) -✅ Acknowledges legitimate disagreement as valid outcome - -**What Tractatus DOES NOT:** -❌ Accept "anything goes" (frameworks must respect human dignity) -❌ Avoid decision-making ("too subjective to choose") -❌ Dismiss deliberation as pointless -❌ Pretend all positions are equally valid regardless of context - ---- - -**Real-world analogy:** - -**Relativism**: Different countries drive on different sides of the road. Neither is "correct." This is preference, not moral truth. - -**Pluralism**: Different cultures have different funeral practices (burial vs. cremation vs. sky burial). Multiple legitimate traditions exist. When traditions conflict (e.g., multicultural family), deliberate with respect for all perspectives, make context-sensitive decision, acknowledge what's lost. - -**Not relativism**: Frameworks that coerce participants (forced burial practices) are not legitimate, even if culturally traditional. - ---- - -**Academic grounding:** - -Tractatus's pluralism draws from: -- **Isaiah Berlin**: Value pluralism (values genuinely conflict, no supervalue) -- **Ruth Chang**: Incommensurability ≠ incomparability -- **Iris Marion Young**: Inclusive deliberation across difference -- **Gutmann & Thompson**: Deliberative democracy with legitimate disagreement - -This is substantive philosophical position, not "anything goes" relativism. - -See [Pluralistic Values Research Foundations](/downloads/pluralistic-values-research-foundations.pdf) for full academic context`, - audience: ['researcher', 'leader'], - keywords: ['relativism', 'pluralism', 'difference', 'philosophy', 'moral', 'ethics', 'comparison'] - }, - { - id: 18, - question: "How does Tractatus adapt communication for different cultural backgrounds?", - answer: `Tractatus includes **AdaptiveCommunicationOrchestrator** to prevent linguistic hierarchy in deliberation: - -**The Problem:** - -If AI governance only communicates in formal academic English, it: -- Excludes non-academics, working-class communities, non-English speakers -- Imposes Western liberal communication norms -- Contradicts pluralistic values (respecting diverse perspectives) - -**Linguistic hierarchy is values hierarchy in disguise.** - -**The Solution: Adaptive Communication** - -Same deliberation outcome, communicated differently based on stakeholder background. - ---- - -**Communication styles detected and respected:** - -**1. Australian/New Zealand norms:** -- **Characteristics**: Directness, anti-tall-poppy syndrome, brevity, casualness -- **Example adaptation**: - - ❌ Formal: "We would be most grateful if you could provide your esteemed perspective..." - - ✅ Direct: "Right, what do you reckon about this approach? Fair?" - -**2. Academic/Research norms:** -- **Characteristics**: Formal register, citations, nuanced qualifications -- **Example adaptation**: - - ✅ Formal: "Thank you for your principled contribution grounded in privacy rights theory (Nissenbaum, 2009). After careful consideration of all perspectives, we have prioritized harm prevention in this context." - -**3. Japanese norms:** -- **Characteristics**: Honne/tatemae (public/private positions), formal register, silence meaningful -- **Example adaptation**: - - Respect for formal communication - - Allow silence without rushing - - Distinguish stated position (tatemae) from underlying concerns (honne) - -**4. Māori protocols (Te Reo Māori + tikanga):** -- **Characteristics**: Mihi (greetings), whanaungatanga (relationships), collective framing -- **Example adaptation**: - - ✅ "Kia ora [Name]. Ngā mihi for bringing the voice of your whānau to this kōrero. Your whakaaro about collective responsibility deeply influenced this decision. Kei te pai?" - -**5. High-context vs. Low-context cultures (Edward Hall):** -- **High-context**: Much meaning from context, indirect communication (Japan, Indigenous cultures) -- **Low-context**: Explicit communication, directness valued (US, Australia, Germany) -- **Adaptation**: Match directness level to cultural preference - ---- - -**Anti-Patronizing Filter (inst\_030):** - -Blocks condescending language patterns BEFORE sending: -- ❌ "Simply..." -- ❌ "Obviously..." -- ❌ "As you may know..." -- ❌ "It's easy to..." -- ❌ Explaining basics to experts -- ❌ Oversimplification when detail requested - -**Why this matters:** -Patronizing language reproduces power imbalances. Saying "Let me explain this simply" to community organizers implies they're less intelligent than academics—FALSE. They have different expertise (community knowledge vs. academic theory), equal respect required. - ---- - -**Real example of adaptive communication:** - -**Decision**: Prioritize safety over privacy in imminent threat context - -**Communicated to each stakeholder:** - -**To privacy researcher (academic):** -\`\`\` -Thank you for your principled contribution grounded in privacy rights -theory and contextual integrity (Nissenbaum, 2009). After structured -deliberation across multiple moral frameworks, we have prioritized harm -prevention in this specific context (imminent threat + exhausted -alternatives). - -Your concerns regarding precedent and surveillance creep have been fully -documented and will inform future deliberations. This decision is scoped -to imminent life-threatening situations and does not establish a general -rule for routine data disclosure. - -Review date: 2026-04-12 -\`\`\` - -**To Australian community organizer (direct):** -\`\`\` -Right, here's where we landed: Save lives first, but only when it's -genuinely urgent and we've tried everything else. - -Your point about trust was spot on—that's exactly why we're not making -this a blanket rule. Next time something similar comes up, we'll take -another look. - -Fair? -\`\`\` - -**To Māori representative (culturally appropriate):** -\`\`\` -Kia ora [Name], - -Ngā mihi for bringing the voice of your whānau to this kōrero. Your -whakaaro about collective responsibility and the importance of trust as -taonga deeply influenced this decision. - -While we prioritized immediate safety in this case, your reminder that -relationships are foundational will guide how we implement this. - -Kei te pai? -\`\`\` - -**Same decision. Different communication styles. No condescension.** - ---- - -**How detection works:** - -\`\`\`javascript -// Detect stakeholder communication style -function detectCommunicationStyle(stakeholder) { - const indicators = { - email_domain: stakeholder.email.includes('.edu.au') ? 'australian_academic' : null, - language: stakeholder.preferred_language, // 'en-NZ', 'mi', 'ja' - self_identification: stakeholder.role, // 'researcher', 'community_organizer', 'iwi_representative' - prior_interactions: stakeholder.communication_history - }; - - return determineStyle(indicators); -} - -// Adapt message -function adaptMessage(message, style) { - if (style === 'australian_direct') { - return removeFormality(message) + addCasualClosing(); - } else if (style === 'academic_formal') { - return addCitations(message) + formalClosing(); - } else if (style === 'maori_protocol') { - return addMihi() + addCollectiveFraming(message) + addMaoriClosing(); - } - // ... other styles -} -\`\`\` - ---- - -**Multilingual support (inst\_032):** - -When stakeholder's preferred language detected: -1. Respond in sender's language (if Claude capable) -2. If not capable: Acknowledge respectfully + offer translation - - "Kia ora! I detected [language] but will respond in English. Translation resources: [link]" -3. For multilingual deliberations: - - Simultaneous translation - - Extra time for comprehension - - Check understanding both directions - ---- - -**"Isn't this condescending—'dumbing down' for some audiences?"** - -**No:** -1. **Different ≠ Dumber** - - Direct language isn't "simplified"—it's preferred style in Australian/NZ culture - - Communal framing isn't "primitive"—it's sophisticated Māori worldview - - Formal academic language isn't inherently "smarter"—it's one cultural style - -2. **Assumes intelligence across styles:** - - Community organizers know their communities better than academics - - Māori representatives are experts in tikanga Māori - - Different knowledge, equal respect - -3. **Anti-patronizing filter prevents condescension** - -**The actual condescension is assuming everyone should communicate like Western academics.** - ---- - -**Instructions enforcing this:** - -- **inst\_029**: Adaptive Communication Tone (match stakeholder style) -- **inst\_030**: Anti-Patronizing Language Filter (block condescending patterns) -- **inst\_031**: Regional Communication Norms (Australian/NZ, Japanese, Māori protocols) -- **inst\_032**: Multilingual Engagement Protocol (language accommodation) - -**Integration:** -AdaptiveCommunicationOrchestrator supports PluralisticDeliberationOrchestrator—ensuring communication doesn't exclude stakeholders through linguistic/cultural barriers. - -See [Value Pluralism FAQ](/downloads/value-pluralism-faq.pdf) Section "Communication & Culture"`, - audience: ['researcher', 'implementer', 'leader'], - keywords: ['communication', 'cultural', 'adaptive', 'language', 'multilingual', 'hierarchy', 'styles'] - }, - // LEADER-SPECIFIC QUESTIONS - { - id: 1, - question: "What is Tractatus Framework in one paragraph?", - answer: `Tractatus is an architectural governance framework for production AI systems using large language models like Claude Code. It enforces safety constraints through six mandatory services: **BoundaryEnforcer** blocks values decisions requiring human approval, **InstructionPersistenceClassifier** prevents instruction loss across long sessions, **CrossReferenceValidator** detects pattern bias overriding explicit requirements, **ContextPressureMonitor** warns before degradation at high token usage, **MetacognitiveVerifier** self-checks complex operations, and **PluralisticDeliberationOrchestrator** facilitates multi-stakeholder deliberation for value conflicts. Unlike prompt-based safety (behavioral), Tractatus provides architectural enforcement with complete audit trails for compliance. Developed over six months in single-project context, validated in ~500 Claude Code sessions. Open-source research implementation, not commercial product. - -**Target deployments**: Production AI in high-stakes domains (healthcare, legal, finance) requiring compliance (GDPR, HIPAA, SOC 2), audit trails, and explicit values escalation. - -See [Introduction](/downloads/introduction-to-the-tractatus-framework.pdf) for 20-page overview or [Technical Architecture](/downloads/technical-architecture-diagram.pdf) for visual summary.`, - audience: ['leader'], - keywords: ['summary', 'overview', 'what is', 'introduction', 'executive', 'brief', 'definition'] - }, - { - id: 2, - question: "We're deploying Copilot across our organisation for client correspondence—what governance gaps should concern us, and how does Tractatus address them?", - answer: `This deployment pattern raises structural questions about governance that existing tools may not address. Here's the architectural concern: - -**The Governance Gap:** - -Copilot for client correspondence operates as an assistive tool. This creates architectural characteristics that may be relevant for organisations subject to regulatory oversight: - -- **No enforced boundaries**: The system can suggest commitments or promises without structural constraints -- **Limited audit trails**: Standard deployment doesn't create evidence of what governance checks occurred (or didn't) -- **No escalation mechanism**: The system cannot detect when a response might require legal review -- **Compliance questions**: GDPR Article 22 (automated decision-making oversight) and SOC 2 CC2.1 (control specification) reference architecturally enforced controls, not voluntary compliance - -The governance concern isn't primarily whether the AI makes errors—it's whether you can demonstrate to regulators that effective oversight was structurally in place. - -**Structural Concerns in Client Correspondence:** - -**1. Commitment Language** -AI-assisted drafting may include language that creates contractual obligations (delivery dates, service commitments, refund promises). If employees approve responses without catching subtle commitment language, and clients rely on those commitments, contractual questions may arise. Post-incident investigations often focus on "what controls were in place?" rather than "who made the error?" - -**2. Cross-Client Information Flow** -LLMs work by pattern completion. When Client A's matter resembles Client B's, the model may draw on similar contexts. Whether this constitutes a confidentiality breach depends on your jurisdiction and client agreements. The structural question is whether your architecture can detect and prevent this, not just rely on human review catching it. - -**3. Regulatory Oversight Requirements** -GDPR Article 22 and similar frameworks require "meaningful human oversight" of automated decision-making. What constitutes "meaningful" is evolving in case law. If your oversight consists of "employee reviews AI output before sending," regulatory questions arise: How do you prove the review occurred? What criteria did they apply? Was it structurally enforced or voluntary? - -**4. Organisational Risk** -AI-assisted responses that are legally correct but contextually inappropriate (tone-deaf responses to vulnerable clients, for example) may create reputational concerns. The governance question is whether your architecture can detect context that requires human judgment, or whether you rely entirely on employee discretion. - -**Where Tractatus May Be Relevant:** - -Tractatus explores whether governance can be architecturally external to the AI system—difficult to bypass through system design rather than voluntary compliance. - -**BoundaryEnforcer** — Intended to detect patterns in responses that may require escalation (commitment language, legal implications, confidential references). In our single-project validation, this service successfully intercepted responses requiring human review before execution. - -**InstructionPersistenceClassifier** — Maintains organisational policies across AI sessions in persistent storage that AI prompts cannot modify. Examples from our deployment: -- "Delivery dates require order confirmation" -- "Regulatory inquiries require legal review" -- "Client identifying information segregated per matter" - -**CrossReferenceValidator** — Validates responses against your governance rules before execution. Creates structured audit logs showing: -- Which rules were checked -- What validation occurred -- Whether escalation was triggered -- Why the response was approved or blocked - -This architectural approach differs from relying on AI to voluntarily invoke governance checks. - -**ContextPressureMonitor** — Tracks factors that may correlate with increased error risk (token usage, conversation length, task complexity). In our validation, this successfully warned when session quality degradation suggested manual review would be prudent. - -**Audit Trail Approach** - -The system creates timestamped logs of governance activity. These logs are external to the AI runtime—they cannot be bypassed by clever prompting or modified retroactively. Whether this constitutes "compliance-grade" evidence depends on your regulatory context, but it provides structural documentation of what governance checks occurred. - -**Potential Implementation Approach:** - -**Phase 1: Observation Mode** -Run Tractatus alongside Copilot without blocking anything. The system logs what governance checks would have been triggered. This generates data about your deployment's governance gap without disrupting workflow. - -**Phase 2: Soft Enforcement** -System warns employees when responses trigger governance rules. They can override (with logging). This phase helps refine rules and identify false positives. - -**Phase 3: Architectural Enforcement** -System blocks responses that fail governance checks and routes them to appropriate reviewers. This creates the architectural control layer. - -**Development Context:** - -Tractatus is a proof-of-concept validated in a single project context (this website). It has not undergone multi-organisation deployment, independent security audit, or regulatory review. Implementation costs will vary significantly based on your technical environment, existing systems, and governance requirements. - -We cannot provide general cost-benefit claims because organisations' risk profiles, incident costs, and regulatory contexts differ substantially. A confidentiality breach may cost one organisation £50k in remediation while another faces £5M in regulatory fines and reputation damage—these variables make universal ROI calculations misleading. - -**Framing for Leadership:** - -The structural question is: "How do we demonstrate to regulators that we had effective governance over AI-assisted client correspondence?" - -Three approaches exist: -1. **Voluntary compliance**: Train employees, create policies, hope they're followed -2. **Post-hoc review**: Sample outputs after they're sent, investigate failures -3. **Architectural enforcement**: Governance checks occur before execution, creating audit trail - -Tractatus explores the third approach. Whether this is necessary for your organisation depends on your regulatory obligations, risk appetite, and existing governance infrastructure. - -**What This Framework Is Not:** - -Tractatus does not replace legal review, compliance expertise, or human judgment. It provides structural enforcement of rules that humans define. If your rules are inadequate or your reviewers make poor decisions, Tractatus enforces those inadequacies architecturally. - -**Critical Distinction:** - -Microsoft's responsible AI principles describe aspirational governance ("we aim to ensure..."). Tractatus explores architectural governance ("system cannot execute unless..."). These are complementary approaches, not alternatives. - -**Exploring Further:** - -If your organisation is evaluating architectural governance approaches for Copilot deployments: - -1. **Review our technical documentation** to understand the architectural pattern -2. **Assess your regulatory context** to determine if architectural enforcement is relevant -3. **Consider your existing governance infrastructure** and where structural gaps may exist - -We're interested in organisations exploring structured governance approaches. Contact research@agenticgovernance.digital if you're evaluating these questions. - -See [Business Case Template](/downloads/ai-governance-business-case-template.pdf) for framework to assess whether architectural governance is relevant to your context.`, - audience: ['leader'], - keywords: ['copilot', 'microsoft', 'client', 'correspondence', 'deployment', 'governance', 'risk', 'liability', 'compliance', 'audit', 'general counsel', 'legal'] - }, - { - id: 3, - question: "How do I justify Tractatus investment to my board?", - answer: `Frame Tractatus as risk mitigation investment using board-appropriate language: - -**Business Case Structure:** - -**1. Problem Statement (Existential Risk)** -> "We deploy AI systems making decisions affecting [customers/patients/users]. Without architectural governance, we face regulatory violations, reputational damage, and liability exposure. Current approach (prompts only) provides no audit trail, no compliance proof, no enforcement mechanisms." - -**Quantify risk:** -- GDPR violations: €20M or 4% revenue (whichever higher) -- SOC 2 audit failure: Loss of enterprise customers (£X million revenue) -- Reputational damage: Brand erosion, customer churn -- Legal liability: Negligence claims from AI failures - -**2. Solution (Architectural Insurance)** -> "Tractatus provides architectural safety layer with compliance-grade audit trails. Six services enforce boundaries before execution—not after failure." - -**Key differentiators:** -- Enforcement (not behavioral) -- Auditable (compliance-provable) -- Preventative (blocks before execution) - -**3. Investment Required** -- **Year 1**: £14,400-33,000 (implementation + ongoing) -- **Year 2+**: £12,400-26,600/year -- **Staff time**: 1-2 days engineering, 4-8 hours domain experts - -**4. Expected Return** -- **Risk mitigation**: Prevents regulatory violations (£400k+ fines) -- **Compliance confidence**: Audit-ready trails for GDPR, SOC 2, HIPAA -- **Operational efficiency**: Automated enforcement reduces manual oversight 60-80% -- **Competitive advantage**: "Governed AI" differentiation in RFPs - -**5. Implementation Plan** -- **Phase 1 (Month 1)**: Pilot with BoundaryEnforcer only (minimal investment) -- **Phase 2 (Month 2-3)**: Full deployment with audit trails -- **Phase 3 (Month 4+)**: Expand to additional AI systems - -**Board-Ready Talking Points:** - -**For Risk-Averse Board:** -> "This is insurance against catastrophic AI failures. Tractatus cost (£25k/year) is 6% of potential GDPR fine (£400k). We cannot prove compliance without it." - -**For Growth-Focused Board:** -> "Enterprise customers require SOC 2 compliance. Tractatus provides audit-ready governance infrastructure enabling us to compete for £X million enterprise deals." - -**For Cost-Conscious Board:** -> "Current approach: Manual AI oversight costs £X per session. Tractatus automates 80% of governance checks, reducing oversight costs by £Y annually while improving reliability." - -**For Innovation-Focused Board:** -> "Governed AI is competitive differentiation. Tractatus enables responsible AI innovation—deploy faster with confidence we won't cause regulatory incidents." - -**Anticipate Objections:** - -**Objection**: "Can't we just use better prompts?" -**Response**: "Prompts guide behaviour, Tractatus enforces architecture. Under context pressure (50k+ tokens), prompts degrade. Tractatus maintains structural enforcement. We need both." - -**Objection**: "This seems expensive for early-stage company." -**Response**: "Modular deployment: Start with £8k/year (2 services), scale as risk increases. One GDPR violation costs 50x this investment." - -**Objection**: "How do we know this works?" -**Response**: "Validated in 6-month deployment, ~500 sessions. Prevented 12 governance failures, 100% values decision protection. Reference implementation available for technical review." - -**Objection**: "What if the framework discontinues?" -**Response**: "Open-source architecture, governance rules stored in our MongoDB, full implementation visibility. No vendor lock-in—we control infrastructure." - -**Financial Summary Slide:** - -| Investment | Year 1 | Year 2+ | -|------------|--------|---------| -| Tractatus | £25,000 | £20,000 | -| **vs.** | | | -| Single GDPR violation | £400,000+ | — | -| SOC 2 audit failure | Lost revenue | — | -| Manual governance overhead | £50,000/year | £50,000/year | - -**ROI**: 300-1,600% if prevents single regulatory incident - -**Decision Point:** -> "We're deploying production AI affecting [customers/patients/users]. The question isn't 'Can we afford Tractatus governance?' but 'Can we afford NOT to have architectural safety enforcement?'" - -**Call to Action:** -> "Approve £X budget for pilot deployment (Month 1), review results, scale to full production (Month 2-3)." - -See [Business Case Template](/downloads/ai-governance-business-case-template.pdf) for customisable financial model and [Executive Brief](/downloads/structural-governance-for-agentic-ai-tractatus-inflection-point.pdf) for strategic context.`, - audience: ['leader'], - keywords: ['board', 'justify', 'business case', 'roi', 'investment', 'approval', 'executives', 'stakeholders'] - }, - { - id: 4, - question: "What happens if Tractatus fails? Who is liable?", - answer: `Tractatus does not eliminate liability—it provides evidence of reasonable governance measures: - -**Liability Framework:** - -**1. What Tractatus Provides:** -✅ **Architectural safeguards**: Six-service enforcement layer demonstrating due diligence -✅ **Audit trails**: Complete records of governance enforcement for legal defence -✅ **Human escalation**: Values decisions escalated to human approval (reduces automation liability) -✅ **Documentation**: Governance rules, enforcement logs, decision rationales -✅ **Good faith effort**: Demonstrates organisation took reasonable steps to prevent AI harms - -**2. What Tractatus Does NOT Provide:** -❌ **Legal shield**: Framework doesn't eliminate liability for AI harms -❌ **Absolute certainty**: No software can prevent all failures -❌ **Insurance/indemnification**: No liability transfer to framework developers -❌ **Compliance certification**: Architecture may support compliance—not certified compliance - -**3. If Tractatus Fails to Prevent Harm:** - -**Legal Position:** -Organisations deploying AI systems remain liable for harms. Tractatus is tool for risk mitigation, not liability elimination. - -**However, audit trail demonstrates:** -- Organisation implemented architectural safeguards (industry best practice) -- Values decisions escalated to human review (not fully automated) -- Governance rules documented and actively enforced -- Regular monitoring via pressure checks and audit logs - -**This reduces negligence risk:** -- **With Tractatus**: "We implemented architectural governance, audit trails show enforcement, human approval for values decisions. This was unforeseeable edge case." -- **Without Tractatus**: "We relied on prompts. No audit trail. No enforcement mechanisms. No evidence of governance." - -**4. Liability Scenarios:** - -**Scenario A: Tractatus blocked action, human overrode, harm occurred** -- **Liability**: Primarily human decision-maker (informed override) -- **Tractatus role**: Audit log shows framework blocked, human approved -- **Defence strength**: Strong (demonstrated governance + informed consent) - -**Scenario B: Tractatus failed to detect values decision, harm occurred** -- **Liability**: Organisation deploying AI + potentially Tractatus developers (if negligence proven) -- **Tractatus role**: Audit log shows framework didn't flag -- **Defence strength**: Moderate (demonstrated governance effort, but failure mode) - -**Scenario C: No Tractatus, AI caused harm** -- **Liability**: Organisation deploying AI -- **Defence strength**: Weak (no governance evidence, no audit trail, no due diligence) - -**5. Insurance and Indemnification:** - -**Current state:** -- **No commercial AI governance insurance** for frameworks like Tractatus -- **Professional indemnity insurance** may cover AI deployment negligence -- **Cyber insurance** may cover data breaches from AI failures - -**Tractatus impact on insurance:** -- Demonstrates due diligence (may reduce premiums) -- Audit trails support claims defence -- Does NOT provide indemnification - -**We recommend:** -- Consult insurance broker about AI governance coverage -- Professional indemnity insurance covering AI deployments -- Verify audit trail quality meets insurance requirements - -**6. Regulatory Liability (GDPR, HIPAA, etc.):** - -**Tractatus benefits:** -- **GDPR Article 22**: Audit shows human approval for automated decisions -- **GDPR Article 35**: Framework demonstrates privacy-by-design -- **HIPAA**: Audit trails show access controls and governance enforcement -- **SOC 2**: Logs demonstrate security controls - -**Development context:** -Framework has not undergone formal compliance audit. Organisations must validate audit trail quality meets their specific regulatory requirements with legal counsel. - -**7. Contractual Liability:** - -**B2B contracts:** -If deploying AI for enterprise customers, contracts likely require governance measures. Tractatus provides: -- Evidence of technical safeguards -- Audit trails for customer review -- Governance rule transparency - -**Example contract language:** -> "Vendor implements architectural AI governance framework with audit trails, human approval for values decisions, and pattern bias detection." - -Tractatus satisfies technical requirements—legal review required for specific contracts. - -**8. Developer Liability (Tractatus Project):** - -**Legal disclaimer:** -Tractatus provided "AS IS" without warranty (standard open-source licence). Developers not liable for deployment failures. - -**However:** -If negligence proven (known critical bug ignored, false claims of capability), developers could face liability. Tractatus mitigates this via: -- Honest development context statements (early-stage research) -- Accurate maturity statements (research, not commercial) -- Open-source visibility (no hidden behaviour) - -**9. Risk Mitigation Recommendations:** - -**Reduce organisational liability:** -✅ Implement Tractatus (demonstrates due diligence) -✅ Document governance rules in version control (provable intent) -✅ Regular audit log reviews (oversight evidence) -✅ Human approval for all values decisions (reduces automation liability) -✅ Legal counsel review of audit trail quality -✅ Professional indemnity insurance covering AI deployments - -**Core principle:** -Tractatus shifts liability defence from "We tried our best with prompts" to "We implemented industry-standard architectural governance with complete audit trails demonstrating enforcement and human oversight." - -**This improves legal position but doesn't eliminate liability.** - -**Questions for your legal counsel:** -1. Does Tractatus audit trail quality meet our regulatory requirements? -2. What additional measures needed for full liability protection? -3. Does our professional indemnity insurance cover AI governance failures? -4. Should we disclose Tractatus governance to customers/users? - -See [Implementation Guide](/downloads/implementation-guide.pdf) Section 7: "Legal and Compliance Considerations" for detailed analysis.`, - audience: ['leader'], - keywords: ['liability', 'legal', 'failure', 'risk', 'insurance', 'responsibility', 'indemnification', 'negligence'] - }, - { - id: 5, - question: "What governance metrics can I report to board and stakeholders?", - answer: `Tractatus provides quantifiable governance metrics for board reporting and stakeholder transparency: - -**Key Performance Indicators (KPIs):** - -**1. Enforcement Effectiveness** -- **Values decisions blocked**: Number of times BoundaryEnforcer blocked values decisions requiring human approval - - **Target**: 100% escalation rate (no values decisions automated) - - **Board metric**: "X values decisions escalated to human review (100% compliance)" - -- **Pattern bias incidents prevented**: CrossReferenceValidator blocks overriding explicit instructions - - **Target**: Zero pattern bias failures - - **Board metric**: "Y instruction conflicts detected and prevented" - -- **Human override rate**: Percentage of blocked decisions approved by humans - - **Benchmark**: 20-40% (shows framework not over-blocking) - - **Board metric**: "Z% of flagged decisions approved after review (appropriate sensitivity)" - -**2. Operational Reliability** -- **Session handoffs completed**: Successful governance continuity across 200k token limit - - **Target**: 100% success rate - - **Board metric**: "X session handoffs completed without instruction loss" - -- **Framework uptime**: Percentage of time all 6 services operational - - **Target**: 99%+ - - **Board metric**: "99.X% governance framework availability" - -- **Pressure warnings issued**: ContextPressureMonitor early warnings before degradation - - **Target**: Warnings issued at 50k, 100k, 150k tokens - - **Board metric**: "X degradation warnings issued, Y handoffs triggered proactively" - -**3. Audit and Compliance** -- **Audit log completeness**: Percentage of AI actions logged - - **Target**: 100% - - **Board metric**: "Complete audit trail for X AI sessions (GDPR Article 30 compliance)" - -- **Rule enforcement consistency**: Percentage of governance rules enforced without exception - - **Target**: 100% - - **Board metric**: "100% consistency across Y rule enforcement events" - -- **Audit-ready documentation**: Days to produce compliance report - - **Target**: <1 day (automated export) - - **Board metric**: "Compliance reports generated in <1 hour (SOC 2 audit-ready)" - -**4. Risk Mitigation** -- **Prevented failures**: Critical incidents blocked by framework - - **Valuation**: Prevented GDPR violation (€20M fine), SOC 2 failure (lost revenue) - - **Board metric**: "Z critical failures prevented, estimated £X risk mitigated" - -- **Security boundary breaches**: Attempted values decisions without human approval - - **Target**: 0 successful breaches - - **Board metric**: "Zero unauthorised values decisions (100% boundary integrity)" - -**MongoDB Query Examples:** - -\`\`\`javascript -// Q1 2025 Board Report (example queries) - -// 1. Values decisions escalated -const valuesEscalations = await db.audit_logs.countDocuments({ - service: "BoundaryEnforcer", - action: "BLOCK", - quarter: "2025-Q1" -}); -// Report: "87 values decisions escalated to human review" - -// 2. Pattern bias incidents prevented -const patternBiasBlocked = await db.audit_logs.countDocuments({ - service: "CrossReferenceValidator", - action: "BLOCK", - conflict_type: "pattern_bias", - quarter: "2025-Q1" -}); -// Report: "12 pattern bias incidents prevented" - -// 3. Human override rate -const overrides = await db.audit_logs.countDocuments({ - service: "BoundaryEnforcer", - action: "BLOCK", - human_override: true, - quarter: "2025-Q1" -}); -const overrideRate = (overrides / valuesEscalations) * 100; -// Report: "34% of flagged decisions approved after review" - -// 4. Audit trail completeness -const totalSessions = 500; // from session logs -const auditedSessions = await db.audit_logs.distinct("session_id", { quarter: "2025-Q1" }).length; -const completeness = (auditedSessions / totalSessions) * 100; -// Report: "100% audit trail coverage across 500 AI sessions" -\`\`\` - -**Board Dashboard (Quarterly):** - -| Metric | Q1 2025 | Q4 2024 | Target | Status | -|--------|---------|---------|--------|--------| -| Values decisions escalated | 87 | 76 | 100% | ✅ | -| Pattern bias prevented | 12 | 8 | >0 | ✅ | -| Human override rate | 34% | 41% | 20-40% | ✅ | -| Framework uptime | 99.7% | 99.2% | >99% | ✅ | -| Audit trail completeness | 100% | 100% | 100% | ✅ | -| Prevented critical failures | 3 | 2 | >0 | ✅ | -| Estimated risk mitigated | £450k | £280k | N/A | 📊 | - -**Stakeholder Transparency Reporting:** - -**For customers/users:** -> "Our AI systems operate under architectural governance with continuous monitoring. Last quarter: 87 values decisions escalated to human review (100% compliance), 12 pattern bias incidents prevented, complete audit trail maintained." - -**For regulators (GDPR, etc.):** -> "Audit logs demonstrate compliance with GDPR Article 22 (human approval for automated decisions). Export available: [link to compliance report]." - -**For investors:** -> "AI governance framework operational with 99.7% uptime. Prevented 3 critical failures, estimated £450k risk mitigation. Zero regulatory violations year-to-date." - -**Narrative Reporting (Annual Report, Investor Update):** - -**Example language:** -> "Tractatus Framework, our architectural AI governance system, completed its first full year of production operation. Across 2,000 AI sessions, the framework escalated 340 values decisions to human review (achieving 100% compliance with our governance standards), prevented 45 pattern bias incidents, and maintained complete audit trails supporting GDPR Article 30 compliance. -> -> No AI-related regulatory violations occurred during this period. Framework uptime exceeded 99.5%, with all six governance services operational. Estimated risk mitigation: £1.2 million in prevented regulatory fines and reputational damage. -> -> Our commitment to responsible AI deployment differentiates us in enterprise sales, with 78% of RFP responses citing governance architecture as competitive advantage." - -**Red Flags to Monitor:** - -🚨 **Human override rate >60%**: Framework over-blocking (tune sensitivity) -🚨 **Human override rate <10%**: Framework under-blocking (strengthen rules) -🚨 **Zero pattern bias incidents**: May indicate CrossReferenceValidator not active -🚨 **Audit trail gaps**: Compliance risk, investigate service failures -🚨 **Framework uptime <95%**: Infrastructure investment needed - -**Export Scripts:** - -\`\`\`bash -# Generate quarterly board report -node scripts/generate-board-report.js --quarter 2025-Q1 --format pdf -# Output: governance-metrics-2025-Q1.pdf - -# Export for compliance audit -node scripts/export-audit-logs.js --start-date 2025-01-01 --end-date 2025-03-31 --format csv -# Output: audit-logs-Q1-2025.csv - -# Stakeholder transparency report -node scripts/generate-transparency-report.js --quarter 2025-Q1 --audience public -# Output: transparency-report-Q1-2025.md -\`\`\` - -**Core Principle:** -Tractatus metrics demonstrate governance effectiveness, not just technical performance. Frame reporting around risk mitigation, compliance confidence, and stakeholder trust—not just "blocks" and "logs." - -See [Audit Guide](/downloads/implementation-guide.pdf) Section 8: "Governance Metrics and Reporting" for complete KPI catalogue.`, - audience: ['leader'], - keywords: ['metrics', 'kpi', 'reporting', 'board', 'dashboard', 'stakeholders', 'measurement', 'performance'] - }, - { - id: 6, - question: "Which regulations does Tractatus help with?", - answer: `Tractatus provides architectural infrastructure that may support compliance efforts for multiple regulations: - -**⚠️ Important Disclaimer:** -Tractatus is NOT compliance-certified software. Framework provides audit trails and governance architecture that may support compliance—legal counsel must validate sufficiency for your specific regulatory requirements. - ---- - -**1. GDPR (General Data Protection Regulation)** - -**Relevant Articles:** - -**Article 22: Automated Decision-Making** -> "Data subject has right not to be subject to decision based solely on automated processing." - -**Tractatus support:** -- BoundaryEnforcer blocks values decisions involving personal data -- Human approval required before execution -- Audit logs document all escalations and approvals -- **Compliance claim**: "Our AI systems escalate privacy decisions to human review (Article 22 compliance)" - -**Article 30: Records of Processing Activities** -> "Controller shall maintain record of processing activities under its responsibility." - -**Tractatus support:** -- Audit logs provide complete record of AI actions -- MongoDB \`audit_logs\` collection queryable by date, action, data category -- Automated export for data protection authority requests -- **Compliance claim**: "Complete audit trail maintained for all AI processing activities" - -**Article 35: Data Protection Impact Assessment (DPIA)** -> "Impact assessment required where processing likely to result in high risk." - -**Tractatus support:** -- BoundaryEnforcer enforces privacy-by-design principle -- Audit logs demonstrate technical safeguards -- Governance rules document privacy boundaries -- **Compliance claim**: "Architectural safeguards demonstrate privacy-by-design approach" - -**GDPR Compliance Checklist:** -✅ Human approval for automated decisions affecting individuals -✅ Complete processing records (audit logs) -✅ Technical safeguards for privacy (boundary enforcement) -⚠️ **Still required**: Legal basis for processing, consent mechanisms, right to erasure implementation - ---- - -**2. HIPAA (Health Insurance Portability and Accountability Act)** - -**Relevant Standards:** - -**§ 164.308(a)(1): Security Management Process** -> "Implement policies to prevent, detect, contain security incidents." - -**Tractatus support:** -- BoundaryEnforcer prevents unauthorised PHI access -- Audit logs detect security incidents -- ContextPressureMonitor warns before degradation -- **Compliance claim**: "Architectural controls prevent unauthorised health data access" - -**§ 164.312(b): Audit Controls** -> "Implement hardware, software to record activity in systems containing PHI." - -**Tractatus support:** -- MongoDB audit logs record all AI actions -- 7-year retention configurable -- Tamper-evident (append-only logs) -- **Compliance claim**: "Complete audit trail for all AI interactions with PHI" - -**HIPAA Compliance Checklist:** -✅ Audit controls for AI systems handling PHI -✅ Access controls via BoundaryEnforcer -✅ Integrity controls via CrossReferenceValidator -⚠️ **Still required**: Encryption at rest/transit, business associate agreements, breach notification procedures - ---- - -**3. SOC 2 (Service Organization Control 2)** - -**Relevant Trust Service Criteria:** - -**CC6.1: Logical Access - Authorization** -> "System enforces access restrictions based on authorization." - -**Tractatus support:** -- BoundaryEnforcer enforces governance rules before action execution -- Audit logs document authorisation decisions -- No bypass mechanism for values decisions -- **Compliance claim**: "Governance rules enforced before sensitive operations" - -**CC7.2: System Monitoring** -> "System includes monitoring activities to detect anomalies." - -**Tractatus support:** -- ContextPressureMonitor warns before degradation -- CrossReferenceValidator detects pattern bias -- Audit logs enable anomaly detection -- **Compliance claim**: "Continuous monitoring for AI governance anomalies" - -**CC7.3: Quality Assurance** -> "System includes processes to maintain quality of processing." - -**Tractatus support:** -- MetacognitiveVerifier checks complex operations -- InstructionPersistenceClassifier maintains instruction integrity -- Session handoff protocol prevents quality degradation -- **Compliance claim**: "Quality controls for AI decision-making processes" - -**SOC 2 Compliance Checklist:** -✅ Access controls (boundary enforcement) -✅ Monitoring (pressure + validator checks) -✅ Quality assurance (metacognitive verification) -✅ Audit trail (complete logging) -⚠️ **Still required**: Penetration testing, incident response plan, vulnerability management - ---- - -**4. ISO 27001 (Information Security Management)** - -**Relevant Controls:** - -**A.12.4: Logging and Monitoring** -> "Event logs recording user activities shall be produced, kept, regularly reviewed." - -**Tractatus support:** -- MongoDB audit logs record all governance events -- Queryable by date, service, action, user -- Automated export for security review -- **Compliance claim**: "Comprehensive event logging for AI governance activities" - -**A.18.1: Compliance with Legal Requirements** -> "Appropriate controls identified, implemented to meet legal obligations." - -**Tractatus support:** -- Governance rules encode legal requirements -- BoundaryEnforcer blocks non-compliant actions -- Audit logs demonstrate compliance efforts -- **Compliance claim**: "Legal requirements enforced via governance rules" - ---- - -**5. AI Act (European Union - Proposed)** - -**Relevant Requirements (High-Risk AI Systems):** - -**Article 9: Risk Management System** -> "High-risk AI systems shall be subject to risk management system." - -**Tractatus support:** -- Six-service architecture addresses identified AI risks -- Audit logs document risk mitigation measures -- Human approval for high-risk decisions -- **Compliance claim**: "Architectural risk management for AI systems" - -**Article 12: Record-Keeping** -> "High-risk AI systems shall have logging capabilities." - -**Tractatus support:** -- Complete audit trail in MongoDB -- Automated export for regulatory authorities -- Retention policy configurable per jurisdiction -- **Compliance claim**: "Audit logs meet AI Act record-keeping requirements" - -**Development context:** -AI Act not yet in force. Tractatus architecture designed to support anticipated requirements—final compliance must be validated when regulation enacted. - ---- - -**6. FTC (Federal Trade Commission) - AI Guidance** - -**FTC Principles:** - -**Transparency**: "Companies should be transparent about AI use." -**Tractatus support**: Audit logs demonstrate governance transparency - -**Fairness**: "AI should not discriminate." -**Tractatus support**: PluralisticDeliberationOrchestrator ensures diverse stakeholder input - -**Accountability**: "Companies accountable for AI harms." -**Tractatus support**: Audit trail demonstrates due diligence - ---- - -**Regulatory Summary Table:** - -| Regulation | Tractatus Support | Still Required | Strength | -|------------|-------------------|----------------|----------| -| **GDPR** | Audit trails, human approval, privacy-by-design | Legal basis, consent, data subject rights | Strong | -| **HIPAA** | Audit controls, access controls | Encryption, BAAs, breach notification | Moderate | -| **SOC 2** | Access controls, monitoring, audit trail | Penetration testing, incident response | Strong | -| **ISO 27001** | Logging, legal compliance controls | Full ISMS, risk assessment | Moderate | -| **AI Act (proposed)** | Risk management, record-keeping | Model documentation, transparency | Moderate | -| **FTC** | Transparency, accountability evidence | Fair lending, discrimination testing | Moderate | - ---- - -**What Tractatus Does NOT Provide:** - -❌ **Legal advice**: Consult counsel for regulatory interpretation -❌ **Certification**: No third-party audit or compliance certification -❌ **Complete compliance**: Architectural infrastructure only, not full programme -❌ **Jurisdiction-specific**: Regulations vary by country/region - ---- - -**Recommended Approach:** - -1. **Identify applicable regulations** for your organisation -2. **Consult legal counsel** to map Tractatus capabilities to requirements -3. **Validate audit trail quality** meets regulatory standards -4. **Implement additional controls** where Tractatus insufficient -5. **Document compliance posture** (what Tractatus provides + what else implemented) - -**Example compliance statement:** -> "Our AI systems operate under Tractatus governance framework, providing audit trails supporting GDPR Article 30, SOC 2 CC6.1, and HIPAA § 164.312(b) compliance. Legal counsel has validated audit trail quality meets our regulatory requirements. Additional controls implemented: [encryption, BAAs, incident response plan]." - ---- - -**Tractatus does NOT replace legal compliance programme—it provides architectural foundation that may support compliance efforts.** - -See [Audit Guide](/downloads/implementation-guide.pdf) Section 9: "Regulatory Compliance Mapping" for detailed analysis.`, - audience: ['leader'], - keywords: ['regulations', 'compliance', 'gdpr', 'hipaa', 'soc2', 'legal', 'regulatory', 'standards', 'certification'] - } -]; - -// State management -let currentFilter = 'all'; -let currentSearchQuery = ''; - -/** - * Open the search modal - */ -function openSearchModal() { - const modal = document.getElementById('search-modal'); - const searchInput = document.getElementById('faq-search'); - - modal.classList.add('show'); - - // Focus on search input - setTimeout(() => { - searchInput.focus(); - }, 100); - - // Render initial results - renderFAQs(); -} - -/** - * Close the search modal - */ -function closeSearchModal() { - const modal = document.getElementById('search-modal'); - modal.classList.remove('show'); -} - -/** - * Open search tips modal - */ -function openSearchTipsModal() { - const modal = document.getElementById('search-tips-modal'); - modal.classList.add('show'); -} - -/** - * Close search tips modal - */ -function closeSearchTipsModal() { - const modal = document.getElementById('search-tips-modal'); - modal.classList.remove('show'); -} - -/** - * Setup modal event listeners - */ -function setupModalListeners() { - // Open search modal button - const openBtn = document.getElementById('open-search-modal-btn'); - if (openBtn) { - openBtn.addEventListener('click', openSearchModal); - } - - // Featured question buttons - const featuredBtns = document.querySelectorAll('.featured-question-btn'); - featuredBtns.forEach(btn => { - btn.addEventListener('click', () => { - const searchQuery = btn.dataset.search; - openSearchModal(); - // Set search value after modal opens - setTimeout(() => { - const searchInput = document.getElementById('faq-search'); - if (searchInput) { - searchInput.value = searchQuery; - searchInput.dispatchEvent(new Event('input')); - } - }, 100); - }); - }); - - // Close search modal button - const closeBtn = document.getElementById('search-modal-close-btn'); - if (closeBtn) { - closeBtn.addEventListener('click', closeSearchModal); - } - - // Search tips button - const tipsBtn = document.getElementById('search-tips-btn'); - if (tipsBtn) { - tipsBtn.addEventListener('click', openSearchTipsModal); - } - - // Close search tips button - const tipCloseBtn = document.getElementById('search-tips-close-btn'); - if (tipCloseBtn) { - tipCloseBtn.addEventListener('click', closeSearchTipsModal); - } - - // Clear filters button - const clearBtn = document.getElementById('clear-filters-btn'); - if (clearBtn) { - clearBtn.addEventListener('click', () => { - currentFilter = 'all'; - currentSearchQuery = ''; - document.getElementById('faq-search').value = ''; - document.getElementById('filter-audience').value = 'all'; - renderFAQs(); - }); - } - - // Close modal on backdrop click - const searchModal = document.getElementById('search-modal'); - if (searchModal) { - searchModal.addEventListener('click', (e) => { - if (e.target === searchModal) { - closeSearchModal(); - } - }); - } - - const tipsModal = document.getElementById('search-tips-modal'); - if (tipsModal) { - tipsModal.addEventListener('click', (e) => { - if (e.target === tipsModal) { - closeSearchTipsModal(); - } - }); - } - - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - // Escape key closes modals - if (e.key === 'Escape') { - closeSearchModal(); - closeSearchTipsModal(); - } - }); -} - -/** - * Render top FAQs inline on the main page (expandable) - */ -function renderInlineFAQs() { - const container = document.getElementById('inline-faq-container'); - if (!container) { - console.error('[FAQ] inline-faq-container not found'); - return; - } - - // Get top 6 most important FAQs (mix of all audiences) - const topFAQs = FAQ_DATA.filter(faq => [19, 12, 27, 13, 1, 2].includes(faq.id)); - - console.log(`[FAQ] Rendering ${topFAQs.length} inline FAQs (marked available: ${typeof marked !== 'undefined'})`); - - // Sort by ID to maintain order - const sorted = topFAQs.sort((a, b) => a.id - b.id); - - // Render as expandable items - container.innerHTML = sorted.map(faq => createInlineFAQItemHTML(faq)).join(''); - - // Add click listeners for expand/collapse - document.querySelectorAll('.inline-faq-question').forEach(question => { - question.addEventListener('click', () => { - const item = question.closest('.inline-faq-item'); - const wasOpen = item.classList.contains('open'); - - // Close all other items - document.querySelectorAll('.inline-faq-item').forEach(other => { - other.classList.remove('open'); - }); - - // Toggle this item (only if it wasn't already open) - if (!wasOpen) { - item.classList.add('open'); - } - }); - }); - - // Apply syntax highlighting to code blocks - if (typeof hljs !== 'undefined') { - document.querySelectorAll('.inline-faq-answer-content pre code').forEach((block) => { - hljs.highlightElement(block); - }); - } - - // Enable markdown links - document.querySelectorAll('.inline-faq-answer-content a').forEach(link => { - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer'); - }); -} - -/** - * Create HTML for inline FAQ item (expandable on main page) - */ -function createInlineFAQItemHTML(faq) { - const audienceColors = { - 'researcher': 'border-purple-200 hover:border-purple-300', - 'implementer': 'border-blue-200 hover:border-blue-300', - 'leader': 'border-green-200 hover:border-green-300' - }; - - const primaryAudience = faq.audience[0]; - const colorClass = audienceColors[primaryAudience] || 'border-gray-200 hover:border-gray-300'; - - // Parse markdown answer - let answerHtml = faq.answer; - if (typeof marked !== 'undefined') { - try { - answerHtml = marked.parse(faq.answer); - } catch (error) { - console.error('[FAQ] Inline markdown parsing failed for FAQ', faq.id, error); - // Fallback to plain text with line breaks - answerHtml = `

${faq.answer.replace(/\n\n/g, '

').replace(/\n/g, '
')}

`; - } - } else { - console.warn('[FAQ] marked.js not loaded for inline FAQs - using plain text'); - // Fallback to plain text with line breaks - answerHtml = `

${faq.answer.replace(/\n\n/g, '

').replace(/\n/g, '
')}

`; - } - - return ` -
-
-

${escapeHtml(faq.question)}

- - - -
-
-
- ${answerHtml} -
-
-
- `; -} - -/** - * Setup category filter button listeners - */ -function setupCategoryButtons() { - const categoryBtns = document.querySelectorAll('.category-filter-btn'); - - categoryBtns.forEach(btn => { - btn.addEventListener('click', () => { - const audience = btn.dataset.audience; - // Set filter and clear search - currentFilter = audience; - currentSearchQuery = ''; - - // Open modal (this will call renderFAQs automatically) - openSearchModal(); - - // Update the filter dropdown to match (UI sync only) - setTimeout(() => { - const filterSelect = document.getElementById('filter-audience'); - if (filterSelect) { - filterSelect.value = audience; - } - }, 50); - }); - }); -} - -/** - * Setup "View All Questions" button listener - */ -function setupViewAllButton() { - const viewAllBtn = document.getElementById('view-all-questions-btn'); - - if (viewAllBtn) { - viewAllBtn.addEventListener('click', () => { - currentFilter = 'all'; - currentSearchQuery = ''; - openSearchModal(); - }); - } -} - -// Initialize on page load -document.addEventListener('DOMContentLoaded', () => { - // Configure marked.js for better rendering - if (typeof marked !== 'undefined') { - marked.setOptions({ - breaks: true, - gfm: true, - headerIds: false - }); - } - - // Render top 6 FAQs inline on page load - renderInlineFAQs(); - - // Setup all event listeners - setupModalListeners(); - setupSearchListener(); - setupFilterListeners(); - setupCategoryButtons(); - setupViewAllButton(); -}); - -/** - * Render FAQ items based on current filter and search - */ -function renderFAQs() { - const container = document.getElementById('faq-container-modal'); - const noResults = document.getElementById('no-results-modal'); - const resultsCount = document.getElementById('search-results-count'); - - // Filter by audience - let filtered = FAQ_DATA; - if (currentFilter !== 'all') { - filtered = FAQ_DATA.filter(faq => faq.audience.includes(currentFilter)); - } - - // Filter by search query - if (currentSearchQuery) { - const query = currentSearchQuery.toLowerCase(); - filtered = filtered.filter(faq => { - const questionMatch = faq.question.toLowerCase().includes(query); - const answerMatch = faq.answer.toLowerCase().includes(query); - const keywordsMatch = faq.keywords.some(kw => kw.includes(query)); - return questionMatch || answerMatch || keywordsMatch; - }); - } - - // Sort by ID (Leader questions have lower IDs, appear first) - filtered = filtered.sort((a, b) => a.id - b.id); - - // Show/hide no results message - if (filtered.length === 0) { - container.classList.add('hidden'); - noResults.classList.remove('hidden'); - resultsCount.textContent = 'No questions found'; - return; - } - - container.classList.remove('hidden'); - noResults.classList.add('hidden'); - - // Update results count - const filterText = currentFilter === 'all' ? 'all questions' : `${currentFilter} questions`; - resultsCount.textContent = `Showing ${filtered.length} of ${FAQ_DATA.length} ${filterText}`; - - console.log(`[FAQ] Rendering ${filtered.length} FAQs in modal (marked available: ${typeof marked !== 'undefined'})`); - - // Render FAQ items (fast, no blocking) - container.innerHTML = filtered.map(faq => createFAQItemHTML(faq)).join(''); - - // Use event delegation for better performance (single listener instead of 31) - container.removeEventListener('click', handleFAQClick); // Remove old listener if exists - container.addEventListener('click', handleFAQClick); - - // Defer expensive syntax highlighting to avoid blocking UI - requestAnimationFrame(() => { - if (typeof hljs !== 'undefined') { - const codeBlocks = container.querySelectorAll('.faq-answer-content pre code'); - // Highlight in small batches to avoid freezing - highlightCodeBlocksInBatches(codeBlocks); - } - }); -} - -/** - * Event delegation handler for FAQ clicks (single listener for all FAQs) - */ -function handleFAQClick(event) { - const question = event.target.closest('.faq-question'); - if (!question) return; - - const item = question.closest('.faq-item'); - if (item) { - item.classList.toggle('open'); - - // Lazy-load syntax highlighting only when FAQ is opened for first time - if (item.classList.contains('open') && !item.dataset.highlighted) { - const codeBlocks = item.querySelectorAll('.faq-answer-content pre code'); - if (codeBlocks.length > 0 && typeof hljs !== 'undefined') { - codeBlocks.forEach(block => hljs.highlightElement(block)); - item.dataset.highlighted = 'true'; - } - } - } -} - -/** - * Highlight code blocks in batches to avoid UI freeze - */ -function highlightCodeBlocksInBatches(codeBlocks, batchSize = 5) { - const blocks = Array.from(codeBlocks); - let index = 0; - - function processBatch() { - const batch = blocks.slice(index, index + batchSize); - batch.forEach(block => { - if (typeof hljs !== 'undefined') { - hljs.highlightElement(block); - } - }); - - index += batchSize; - - if (index < blocks.length) { - // Schedule next batch - requestAnimationFrame(processBatch); - } - } - - if (blocks.length > 0) { - processBatch(); - } -} - -/** - * Create HTML for a single FAQ item - */ -function createFAQItemHTML(faq) { - const highlightedQuestion = highlightText(faq.question, currentSearchQuery); - - // Parse markdown to HTML - let answerHTML = faq.answer; - if (typeof marked !== 'undefined') { - try { - answerHTML = marked.parse(faq.answer); - } catch (error) { - console.error('[FAQ] Markdown parsing failed for FAQ', faq.id, error); - // Fallback to plain text with line breaks - answerHTML = `

${faq.answer.replace(/\n\n/g, '

').replace(/\n/g, '
')}

`; - } - } else { - console.warn('[FAQ] marked.js not loaded - using plain text'); - // Fallback to plain text with line breaks - answerHTML = `

${faq.answer.replace(/\n\n/g, '

').replace(/\n/g, '
')}

`; - } - - // Highlight search query in rendered HTML (if searching) - if (currentSearchQuery) { - const regex = new RegExp(`(${escapeRegex(currentSearchQuery)})`, 'gi'); - answerHTML = answerHTML.replace(regex, '$1'); - } - - // Audience badges - const badges = faq.audience.map(aud => { - const colors = { - researcher: 'bg-purple-100 text-purple-700', - implementer: 'bg-blue-100 text-blue-700', - leader: 'bg-green-100 text-green-700' - }; - return `${aud}`; - }).join(' '); - - return ` -
-
-
-
-

${highlightedQuestion}

-
- ${badges} -
-
-
- -
-
-
-
-
${answerHTML}
-
-
- `; -} - -/** - * Highlight search query in text - */ -function highlightText(text, query) { - if (!query) return escapeHtml(text); - - const regex = new RegExp(`(${escapeRegex(query)})`, 'gi'); - return escapeHtml(text).replace(regex, '$1'); -} - -/** - * Escape HTML to prevent XSS - */ -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -/** - * Escape regex special characters - */ -function escapeRegex(text) { - return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -/** - * Setup search input listener - */ -function setupSearchListener() { - const searchInput = document.getElementById('faq-search'); - - searchInput.addEventListener('input', (e) => { - currentSearchQuery = e.target.value.trim(); - renderFAQs(); - }); -} - -/** - * Setup filter dropdown listener - */ -function setupFilterListeners() { - const audienceFilter = document.getElementById('filter-audience'); - - if (audienceFilter) { - audienceFilter.addEventListener('change', (e) => { - currentFilter = e.target.value; - renderFAQs(); - }); - } -} diff --git a/public/js/i18n-simple.js b/public/js/i18n-simple.js deleted file mode 100644 index b725a8be..00000000 --- a/public/js/i18n-simple.js +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Simple i18n system for Tractatus - * Supports: en (English), de (German), fr (French), mi (Te Reo Māori - coming soon) - */ - -const I18n = { - currentLang: 'en', - translations: {}, - supportedLanguages: ['en', 'de', 'fr'], - - async init() { - // 1. Detect language preference - this.currentLang = this.detectLanguage(); - - // 2. Load translations - await this.loadTranslations(this.currentLang); - - // 3. Apply to page - this.applyTranslations(); - - // 4. Update language selector if present - this.updateLanguageSelector(); - - console.log(`[i18n] Initialized with language: ${this.currentLang}`); - }, - - detectLanguage() { - // Priority order: - // 1. User's manual selection (localStorage) - allows override via flag clicks - // 2. Browser's language setting (automatic detection) - // 3. Default to English (fallback) - - // 1. Check localStorage first (user override) - const saved = localStorage.getItem('tractatus-lang'); - if (saved && this.supportedLanguages.includes(saved)) { - console.log(`[i18n] Language detected from user preference: ${saved}`); - return saved; - } - - // 2. Check browser language (automatic detection) - const browserLang = (navigator.language || navigator.userLanguage).split('-')[0]; - if (this.supportedLanguages.includes(browserLang)) { - console.log(`[i18n] Language detected from browser: ${browserLang} (from ${navigator.language})`); - return browserLang; - } - - // 3. Default to English - console.log(`[i18n] Language defaulted to: en (browser language '${navigator.language}' not supported)`); - return 'en'; - }, - - detectPageName() { - // Try to get page name from data attribute first - const pageAttr = document.documentElement.getAttribute('data-page'); - if (pageAttr) { - return pageAttr; - } - - // Detect from URL path - const path = window.location.pathname; - - // Map paths to translation file names - const pageMap = { - '/': 'homepage', - '/index.html': 'homepage', - '/researcher.html': 'researcher', - '/leader.html': 'leader', - '/implementer.html': 'implementer', - '/about.html': 'about', - '/about/values.html': 'values', - '/about/values': 'values', - '/faq.html': 'faq', - '/koha.html': 'koha', - '/koha/transparency.html': 'transparency', - '/koha/transparency': 'transparency', - '/privacy.html': 'privacy', - '/privacy': 'privacy' - }; - - return pageMap[path] || 'homepage'; - }, - - async loadTranslations(lang) { - try { - // Always load common translations (footer, navbar, etc.) - const commonResponse = await fetch(`/locales/${lang}/common.json`); - let commonTranslations = {}; - if (commonResponse.ok) { - commonTranslations = await commonResponse.json(); - } - - // Load page-specific translations - const pageName = this.detectPageName(); - const pageResponse = await fetch(`/locales/${lang}/${pageName}.json`); - let pageTranslations = {}; - if (pageResponse.ok) { - pageTranslations = await pageResponse.json(); - } else if (pageName !== 'homepage') { - // If page-specific translations don't exist, that's okay for some pages - console.warn(`[i18n] No translations found for ${lang}/${pageName}, using common only`); - } else { - throw new Error(`Failed to load translations for ${lang}/${pageName}`); - } - - // Merge common and page-specific translations (page-specific takes precedence) - this.translations = { ...commonTranslations, ...pageTranslations }; - console.log(`[i18n] Loaded translations: common + ${pageName}`); - } catch (error) { - console.error(`[i18n] Error loading translations:`, error); - // Fallback to English if loading fails - if (lang !== 'en') { - this.currentLang = 'en'; - await this.loadTranslations('en'); - } - } - }, - - t(key) { - const keys = key.split('.'); - let value = this.translations; - - for (const k of keys) { - if (value && typeof value === 'object') { - value = value[k]; - } else { - return key; // Return key if translation not found - } - } - - return value || key; - }, - - applyTranslations() { - // Find all elements with data-i18n attribute - // Using innerHTML to preserve formatting like , , tags in translations - document.querySelectorAll('[data-i18n]').forEach(el => { - const key = el.dataset.i18n; - const translation = this.t(key); - - if (typeof translation === 'string') { - el.innerHTML = translation; - } - }); - - // Handle data-i18n-html for HTML content (kept for backward compatibility) - document.querySelectorAll('[data-i18n-html]').forEach(el => { - const key = el.dataset.i18nHtml; - const translation = this.t(key); - - if (typeof translation === 'string') { - el.innerHTML = translation; - } - }); - - // Handle data-i18n-placeholder for input placeholders - document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { - const key = el.dataset.i18nPlaceholder; - const translation = this.t(key); - - if (typeof translation === 'string') { - el.placeholder = translation; - } - }); - }, - - async setLanguage(lang) { - if (!this.supportedLanguages.includes(lang)) { - console.error(`[i18n] Unsupported language: ${lang}`); - return; - } - - // Save preference (overrides browser language detection) - localStorage.setItem('tractatus-lang', lang); - console.log(`[i18n] User manually selected language: ${lang} (saved to localStorage)`); - - // Update current language - this.currentLang = lang; - - // Reload translations - await this.loadTranslations(lang); - - // Reapply to page - this.applyTranslations(); - - // Update selector - this.updateLanguageSelector(); - - // Update HTML lang attribute - document.documentElement.lang = lang; - - // Dispatch event for language change - window.dispatchEvent(new CustomEvent('languageChanged', { - detail: { language: lang } - })); - - console.log(`[i18n] Language changed to: ${lang} (will persist across pages)`); - }, - - updateLanguageSelector() { - const selector = document.getElementById('language-selector'); - if (selector) { - selector.value = this.currentLang; - } - }, - - getLanguageName(code) { - const names = { - 'en': 'English', - 'de': 'Deutsch', - 'fr': 'Français', - 'mi': 'Te Reo Māori' - }; - return names[code] || code; - } -}; - -// Auto-initialize when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => I18n.init()); -} else { - I18n.init(); -} - -// Make available globally -window.I18n = I18n; diff --git a/public/js/koha-donation.js b/public/js/koha-donation.js deleted file mode 100644 index b04fdb3a..00000000 --- a/public/js/koha-donation.js +++ /dev/null @@ -1,415 +0,0 @@ -/** - * Koha Donation System - * Handles donation form functionality with CSP compliance - */ - -// Form state -let selectedFrequency = 'monthly'; -let selectedTier = '15'; -let selectedAmount = 1500; // in cents (NZD) -let currentCurrency = typeof detectUserCurrency === 'function' ? detectUserCurrency() : 'NZD'; - -document.addEventListener('DOMContentLoaded', function() { - // Initialize event listeners - initializeFrequencyButtons(); - initializeTierCards(); - initializePublicAcknowledgement(); - initializeDonationForm(); -}); - -/** - * Initialize frequency selection buttons - */ -function initializeFrequencyButtons() { - const monthlyBtn = document.getElementById('freq-monthly'); - const onetimeBtn = document.getElementById('freq-onetime'); - - if (monthlyBtn) { - monthlyBtn.addEventListener('click', function() { - selectFrequency('monthly'); - }); - } - - if (onetimeBtn) { - onetimeBtn.addEventListener('click', function() { - selectFrequency('one_time'); - }); - } -} - -/** - * Initialize tier card click handlers - */ -function initializeTierCards() { - const tierCards = document.querySelectorAll('[data-tier]'); - - tierCards.forEach(card => { - card.addEventListener('click', function() { - const tier = this.dataset.tier; - const amount = parseInt(this.dataset.amount); - selectTier(tier, amount); - }); - }); -} - -/** - * Initialize public acknowledgement checkbox - */ -function initializePublicAcknowledgement() { - const checkbox = document.getElementById('public-acknowledgement'); - - if (checkbox) { - checkbox.addEventListener('change', togglePublicName); - } -} - -/** - * Initialize donation form submission - */ -function initializeDonationForm() { - const form = document.getElementById('donation-form'); - - if (form) { - form.addEventListener('submit', handleFormSubmit); - } -} - -/** - * Update prices when currency changes - */ -window.updatePricesForCurrency = function(currency) { - currentCurrency = currency; - - if (typeof getTierPrices !== 'function' || typeof formatCurrency !== 'function') { - console.warn('Currency utilities not loaded'); - return; - } - - const prices = getTierPrices(currency); - - // Update tier card prices - const tierCards = document.querySelectorAll('.tier-card'); - if (tierCards[0]) { - const priceEl = tierCards[0].querySelector('.text-4xl'); - const currencyEl = tierCards[0].querySelector('.text-sm.text-gray-500'); - if (priceEl) priceEl.textContent = formatCurrency(prices.tier_5, currency).replace(/\.\d+$/, ''); - if (currencyEl) currencyEl.textContent = `${currency} / month`; - } - - if (tierCards[1]) { - const priceEl = tierCards[1].querySelector('.text-4xl'); - const currencyEl = tierCards[1].querySelector('.text-sm.text-gray-500'); - if (priceEl) priceEl.textContent = formatCurrency(prices.tier_15, currency).replace(/\.\d+$/, ''); - if (currencyEl) currencyEl.textContent = `${currency} / month`; - } - - if (tierCards[2]) { - const priceEl = tierCards[2].querySelector('.text-4xl'); - const currencyEl = tierCards[2].querySelector('.text-sm.text-gray-500'); - if (priceEl) priceEl.textContent = formatCurrency(prices.tier_50, currency).replace(/\.\d+$/, ''); - if (currencyEl) currencyEl.textContent = `${currency} / month`; - } - - // Update custom amount placeholder - const amountInput = document.getElementById('amount-input'); - if (amountInput) { - amountInput.placeholder = 'Enter amount'; - const amountCurrencyLabel = amountInput.nextElementSibling; - if (amountCurrencyLabel) { - amountCurrencyLabel.textContent = currency; - } - } - - // Update help text - const amountHelp = document.getElementById('amount-help'); - if (amountHelp && typeof formatCurrency === 'function') { - amountHelp.textContent = `Minimum donation: ${formatCurrency(100, currency)}`; - } -}; - -/** - * Select donation frequency (monthly or one-time) - */ -function selectFrequency(freq) { - selectedFrequency = freq; - - // Update button styles - const monthlyBtn = document.getElementById('freq-monthly'); - const onetimeBtn = document.getElementById('freq-onetime'); - - if (freq === 'monthly') { - monthlyBtn.className = 'flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition'; - onetimeBtn.className = 'flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition'; - - // Show tier selection, hide custom amount - const tierSelection = document.getElementById('tier-selection'); - const customAmount = document.getElementById('custom-amount'); - if (tierSelection) tierSelection.classList.remove('hidden'); - if (customAmount) customAmount.classList.add('hidden'); - } else { - monthlyBtn.className = 'flex-1 py-3 px-6 border-2 border-gray-300 bg-white text-gray-700 rounded-lg font-semibold hover:border-blue-600 transition'; - onetimeBtn.className = 'flex-1 py-3 px-6 border-2 border-blue-600 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition'; - - // Hide tier selection, show custom amount - const tierSelection = document.getElementById('tier-selection'); - const customAmount = document.getElementById('custom-amount'); - if (tierSelection) tierSelection.classList.add('hidden'); - if (customAmount) customAmount.classList.remove('hidden'); - - const amountInput = document.getElementById('amount-input'); - if (amountInput) amountInput.focus(); - } -} - -/** - * Select tier amount - */ -function selectTier(tier, amountNZDCents) { - selectedTier = tier; - - // Calculate amount in current currency - if (typeof getTierPrices === 'function') { - const prices = getTierPrices(currentCurrency); - selectedAmount = prices[`tier_${tier}`]; - } else { - selectedAmount = amountNZDCents; - } - - // Update card styles - const cards = document.querySelectorAll('.tier-card'); - cards.forEach(card => { - card.classList.remove('selected'); - }); - - // Add selected class to clicked card - const selectedCard = document.querySelector(`[data-tier="${tier}"]`); - if (selectedCard) { - selectedCard.classList.add('selected'); - } -} - -/** - * Toggle public name field visibility - */ -function togglePublicName() { - const checkbox = document.getElementById('public-acknowledgement'); - const nameField = document.getElementById('public-name-field'); - - if (checkbox && nameField) { - if (checkbox.checked) { - nameField.classList.remove('hidden'); - const publicNameInput = document.getElementById('public-name'); - if (publicNameInput) publicNameInput.focus(); - } else { - nameField.classList.add('hidden'); - } - } -} - -/** - * Handle form submission - */ -async function handleFormSubmit(e) { - e.preventDefault(); - - const submitBtn = e.target.querySelector('button[type="submit"]'); - submitBtn.disabled = true; - submitBtn.textContent = 'Processing...'; - - try { - // Get form data - const donorName = document.getElementById('donor-name').value.trim() || 'Anonymous'; - const donorEmail = document.getElementById('donor-email').value.trim(); - const donorCountry = document.getElementById('donor-country').value.trim(); - const publicAck = document.getElementById('public-acknowledgement').checked; - const publicName = document.getElementById('public-name').value.trim(); - - // Validate email - if (!donorEmail) { - alert('Please enter your email address.'); - submitBtn.disabled = false; - submitBtn.textContent = 'Proceed to Secure Payment'; - return; - } - - // Get amount - let amount; - if (selectedFrequency === 'monthly') { - amount = selectedAmount; - } else { - const customAmount = parseFloat(document.getElementById('amount-input').value); - if (!customAmount || customAmount < 1) { - const minAmount = typeof formatCurrency === 'function' - ? formatCurrency(100, currentCurrency) - : `${currentCurrency} 1.00`; - alert(`Please enter a donation amount of at least ${minAmount}.`); - submitBtn.disabled = false; - submitBtn.textContent = 'Proceed to Secure Payment'; - return; - } - amount = Math.round(customAmount * 100); // Convert to cents - } - - // Create checkout session - const response = await fetch('/api/koha/checkout', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - amount: amount, - currency: currentCurrency, - frequency: selectedFrequency, - tier: selectedFrequency === 'monthly' ? selectedTier : 'custom', - donor: { - name: donorName, - email: donorEmail, - country: donorCountry - }, - public_acknowledgement: publicAck, - public_name: publicAck ? (publicName || donorName) : null - }) - }); - - const data = await response.json(); - - if (data.success && data.data.checkoutUrl) { - // Redirect to Stripe Checkout - window.location.href = data.data.checkoutUrl; - } else { - throw new Error(data.error || 'Failed to create checkout session'); - } - - } catch (error) { - console.error('Donation error:', error); - alert('An error occurred while processing your donation. Please try again or contact support.'); - submitBtn.disabled = false; - submitBtn.textContent = 'Proceed to Secure Payment'; - } -} - -/** - * Initialize manage subscription functionality - */ -document.addEventListener('DOMContentLoaded', function() { - const manageBtn = document.getElementById('manage-subscription-btn'); - const emailInput = document.getElementById('manage-email'); - - if (manageBtn && emailInput) { - manageBtn.addEventListener('click', handleManageSubscription); - emailInput.addEventListener('keypress', function(e) { - if (e.key === 'Enter') { - e.preventDefault(); - handleManageSubscription(); - } - }); - } -}); - -/** - * Handle manage subscription button click - */ -async function handleManageSubscription() { - const emailInput = document.getElementById('manage-email'); - const manageBtn = document.getElementById('manage-subscription-btn'); - const errorDiv = document.getElementById('manage-error'); - const loadingDiv = document.getElementById('manage-loading'); - - const email = emailInput.value.trim(); - - // Validate email - if (!email) { - showManageError('Please enter your email address.'); - emailInput.focus(); - return; - } - - if (!isValidEmail(email)) { - showManageError('Please enter a valid email address.'); - emailInput.focus(); - return; - } - - // Hide error, show loading - hideManageError(); - showManageLoading(); - manageBtn.disabled = true; - - try { - const response = await fetch('/api/koha/portal', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ email }) - }); - - const data = await response.json(); - - if (data.success && data.data.url) { - // Redirect to Stripe Customer Portal - window.location.href = data.data.url; - } else if (response.status === 404) { - showManageError('No subscription found for this email address. Please check your email and try again.'); - manageBtn.disabled = false; - hideManageLoading(); - } else { - throw new Error(data.error || 'Failed to access subscription portal'); - } - - } catch (error) { - console.error('Manage subscription error:', error); - showManageError('An error occurred. Please try again or contact support@agenticgovernance.digital'); - manageBtn.disabled = false; - hideManageLoading(); - } -} - -/** - * Validate email format - */ -function isValidEmail(email) { - const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return re.test(email); -} - -/** - * Show error message - */ -function showManageError(message) { - const errorDiv = document.getElementById('manage-error'); - if (errorDiv) { - errorDiv.textContent = message; - errorDiv.classList.remove('hidden'); - } -} - -/** - * Hide error message - */ -function hideManageError() { - const errorDiv = document.getElementById('manage-error'); - if (errorDiv) { - errorDiv.classList.add('hidden'); - } -} - -/** - * Show loading indicator - */ -function showManageLoading() { - const loadingDiv = document.getElementById('manage-loading'); - if (loadingDiv) { - loadingDiv.classList.remove('hidden'); - } -} - -/** - * Hide loading indicator - */ -function hideManageLoading() { - const loadingDiv = document.getElementById('manage-loading'); - if (loadingDiv) { - loadingDiv.classList.add('hidden'); - } -} diff --git a/public/js/koha-success.js b/public/js/koha-success.js deleted file mode 100644 index f0b96025..00000000 --- a/public/js/koha-success.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Koha Success Page - Donation Verification - */ - -// Get session ID from URL -const urlParams = new URLSearchParams(window.location.search); -const sessionId = urlParams.get('session_id'); - -// Verify donation -async function verifyDonation() { - if (!sessionId) { - // No session ID - just show success message - document.getElementById('loading-content').classList.add('hidden'); - document.getElementById('success-content').classList.remove('hidden'); - return; - } - - try { - const response = await fetch(`/api/koha/verify/${sessionId}`); - const data = await response.json(); - - if (data.success && data.data.isSuccessful) { - // Show success content - document.getElementById('loading-content').classList.add('hidden'); - document.getElementById('success-content').classList.remove('hidden'); - - // Update details - document.getElementById('amount').textContent = `$${data.data.amount.toFixed(2)} ${data.data.currency.toUpperCase()}`; - - const frequencyText = data.data.frequency === 'monthly' ? 'Monthly Donation' : 'One-Time Donation'; - document.getElementById('frequency').textContent = frequencyText; - - // Show monthly section if applicable - if (data.data.frequency === 'monthly') { - document.getElementById('monthly-section').classList.remove('hidden'); - } - - } else { - throw new Error('Donation not successful'); - } - - } catch (error) { - console.error('Verification error:', error); - document.getElementById('loading-content').classList.add('hidden'); - document.getElementById('error-content').classList.remove('hidden'); - } -} - -// Verify on page load -if (sessionId) { - document.getElementById('success-content').classList.add('hidden'); - document.getElementById('loading-content').classList.remove('hidden'); - verifyDonation(); -} diff --git a/public/js/koha-transparency.js b/public/js/koha-transparency.js deleted file mode 100644 index e1c452f3..00000000 --- a/public/js/koha-transparency.js +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Koha Transparency Dashboard - * Real-time donation metrics with privacy-preserving analytics - */ - -// Chart instances (global for updates) -let allocationChart = null; -let trendChart = null; - -/** - * Load and display transparency metrics - */ -async function loadMetrics() { - try { - const response = await fetch('/api/koha/transparency'); - const data = await response.json(); - - if (data.success && data.data) { - const metrics = data.data; - - // Update stats - updateStats(metrics); - - // Update allocation chart - updateAllocationChart(metrics); - - // Update progress bars (legacy display) - animateProgressBars(); - - // Display recent donors - displayRecentDonors(metrics.recent_donors || []); - - // Update last updated time - updateLastUpdated(metrics.last_updated); - - } else { - throw new Error('Failed to load metrics'); - } - - } catch (error) { - console.error('Error loading transparency metrics:', error); - document.getElementById('recent-donors').innerHTML = ` -
- Failed to load transparency data. Please try again later. -
- `; - } -} - -/** - * Update stats display - */ -function updateStats(metrics) { - document.getElementById('total-received').textContent = `$${metrics.total_received.toFixed(2)}`; - document.getElementById('monthly-supporters').textContent = metrics.monthly_supporters; - document.getElementById('monthly-revenue').textContent = `$${metrics.monthly_recurring_revenue.toFixed(2)}`; - document.getElementById('onetime-count').textContent = metrics.one_time_donations || 0; - - // Calculate average - const totalCount = metrics.monthly_supporters + (metrics.one_time_donations || 0); - const avgDonation = totalCount > 0 ? metrics.total_received / totalCount : 0; - document.getElementById('average-donation').textContent = `$${avgDonation.toFixed(2)}`; -} - -/** - * Update allocation pie chart - */ -function updateAllocationChart(metrics) { - const ctx = document.getElementById('allocation-chart'); - if (!ctx) return; - - const allocation = metrics.allocation || { - development: 0.4, - hosting: 0.3, - research: 0.2, - community: 0.1 - }; - - const data = { - labels: ['Development (40%)', 'Hosting & Infrastructure (30%)', 'Research (20%)', 'Community (10%)'], - datasets: [{ - data: [ - allocation.development * 100, - allocation.hosting * 100, - allocation.research * 100, - allocation.community * 100 - ], - backgroundColor: [ - '#3B82F6', // blue-600 - '#10B981', // green-600 - '#A855F7', // purple-600 - '#F59E0B' // orange-600 - ], - borderWidth: 2, - borderColor: '#FFFFFF' - }] - }; - - const config = { - type: 'doughnut', - data: data, - options: { - responsive: true, - maintainAspectRatio: true, - plugins: { - legend: { - position: 'bottom', - labels: { - padding: 15, - font: { - size: 12 - } - } - }, - tooltip: { - callbacks: { - label: function(context) { - const label = context.label || ''; - const value = context.parsed; - return `${label}: ${value.toFixed(1)}%`; - } - } - } - } - } - }; - - if (allocationChart) { - allocationChart.data = data; - allocationChart.update(); - } else { - allocationChart = new Chart(ctx, config); - } -} - -/** - * Animate progress bars (legacy display) - */ -function animateProgressBars() { - setTimeout(() => { - document.querySelectorAll('.progress-bar').forEach(bar => { - const width = bar.getAttribute('data-width'); - bar.style.width = width + '%'; - }); - }, 100); -} - -/** - * Display recent donors - */ -function displayRecentDonors(donors) { - const donorsContainer = document.getElementById('recent-donors'); - const noDonorsMessage = document.getElementById('no-donors'); - - if (donors.length > 0) { - const donorsHtml = donors.map(donor => { - const date = new Date(donor.date); - const dateStr = date.toLocaleDateString('en-NZ', { year: 'numeric', month: 'short' }); - const freqBadge = donor.frequency === 'monthly' - ? 'Monthly' - : 'One-time'; - - // Format currency display - const currency = (donor.currency || 'nzd').toUpperCase(); - const amountDisplay = `$${donor.amount.toFixed(2)} ${currency}`; - - // Show NZD equivalent if different currency - const nzdEquivalent = currency !== 'NZD' - ? `
≈ $${donor.amount_nzd.toFixed(2)} NZD
` - : ''; - - return ` -
-
-
- ${donor.name.charAt(0).toUpperCase()} -
-
-
${donor.name}
-
${dateStr}
-
-
-
-
${amountDisplay}
- ${nzdEquivalent} - ${freqBadge} -
-
- `; - }).join(''); - - donorsContainer.innerHTML = donorsHtml; - donorsContainer.style.display = 'block'; - if (noDonorsMessage) noDonorsMessage.style.display = 'none'; - } else { - donorsContainer.style.display = 'none'; - if (noDonorsMessage) noDonorsMessage.style.display = 'block'; - } -} - -/** - * Update last updated timestamp - */ -function updateLastUpdated(timestamp) { - const lastUpdated = new Date(timestamp); - const elem = document.getElementById('last-updated'); - if (elem) { - elem.textContent = `Last updated: ${lastUpdated.toLocaleString()}`; - } -} - -/** - * Export transparency data as CSV - */ -async function exportCSV() { - try { - const response = await fetch('/api/koha/transparency'); - const data = await response.json(); - - if (!data.success || !data.data) { - throw new Error('Failed to load metrics for export'); - } - - const metrics = data.data; - - // Build CSV content - let csv = 'Tractatus Koha Transparency Report\\n'; - csv += `Generated: ${new Date().toISOString()}\\n\\n`; - - csv += 'Metric,Value\\n'; - csv += `Total Received,${metrics.total_received}\\n`; - csv += `Monthly Supporters,${metrics.monthly_supporters}\\n`; - csv += `One-Time Donations,${metrics.one_time_donations || 0}\\n`; - csv += `Monthly Recurring Revenue,${metrics.monthly_recurring_revenue}\\n\\n`; - - csv += 'Allocation Category,Percentage\\n'; - csv += `Development,${(metrics.allocation.development * 100).toFixed(1)}%\\n`; - csv += `Hosting & Infrastructure,${(metrics.allocation.hosting * 100).toFixed(1)}%\\n`; - csv += `Research,${(metrics.allocation.research * 100).toFixed(1)}%\\n`; - csv += `Community,${(metrics.allocation.community * 100).toFixed(1)}%\\n\\n`; - - if (metrics.recent_donors && metrics.recent_donors.length > 0) { - csv += 'Recent Public Supporters\\n'; - csv += 'Name,Date,Amount,Currency,Frequency\\n'; - metrics.recent_donors.forEach(donor => { - csv += `"${donor.name}",${donor.date},${donor.amount},${donor.currency || 'NZD'},${donor.frequency}\\n`; - }); - } - - // Create download - const blob = new Blob([csv], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `tractatus-transparency-${new Date().toISOString().split('T')[0]}.csv`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - - } catch (error) { - console.error('Error exporting CSV:', error); - alert('Failed to export transparency data. Please try again.'); - } -} - -// Initialize on page load -document.addEventListener('DOMContentLoaded', () => { - // Load metrics - loadMetrics(); - - // Refresh every 5 minutes - setInterval(loadMetrics, 5 * 60 * 1000); - - // Setup CSV export button - const exportBtn = document.getElementById('export-csv'); - if (exportBtn) { - exportBtn.addEventListener('click', exportCSV); - } -}); diff --git a/public/js/leader-page.js b/public/js/leader-page.js deleted file mode 100644 index 4d951884..00000000 --- a/public/js/leader-page.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Leader Page - Accordion Functionality - * Handles expandable/collapsible sections for leadership content - */ - -document.addEventListener('DOMContentLoaded', function() { - // Get all accordion buttons - const accordionButtons = document.querySelectorAll('[data-accordion]'); - - accordionButtons.forEach(button => { - button.addEventListener('click', function() { - const accordionId = this.dataset.accordion; - toggleAccordion(accordionId); - }); - }); - - /** - * Toggle accordion section open/closed - * @param {string} id - Accordion section ID - */ - function toggleAccordion(id) { - const content = document.getElementById(id + '-content'); - const icon = document.getElementById(id + '-icon'); - - if (content && icon) { - content.classList.toggle('active'); - icon.classList.toggle('active'); - } - } -}); diff --git a/public/js/media-inquiry.js b/public/js/media-inquiry.js deleted file mode 100644 index 8abc2afe..00000000 --- a/public/js/media-inquiry.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Media Inquiry Form Handler - */ - -const form = document.getElementById('media-inquiry-form'); -const submitButton = document.getElementById('submit-button'); -const successMessage = document.getElementById('success-message'); -const errorMessage = document.getElementById('error-message'); - -form.addEventListener('submit', async (e) => { - e.preventDefault(); - - // Hide previous messages - successMessage.style.display = 'none'; - errorMessage.style.display = 'none'; - - // Disable submit button - submitButton.disabled = true; - submitButton.textContent = 'Submitting...'; - - // Collect form data - const formData = { - contact: { - name: document.getElementById('contact-name').value, - email: document.getElementById('contact-email').value, - outlet: document.getElementById('contact-outlet').value, - phone: document.getElementById('contact-phone').value || null - }, - inquiry: { - subject: document.getElementById('inquiry-subject').value, - message: document.getElementById('inquiry-message').value, - deadline: document.getElementById('inquiry-deadline').value || null, - topic_areas: [] - } - }; - - try { - const response = await fetch('/api/media/inquiries', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(formData) - }); - - const data = await response.json(); - - if (response.ok) { - // Success - successMessage.textContent = data.message || 'Thank you for your inquiry. We will review and respond shortly.'; - successMessage.style.display = 'block'; - form.reset(); - window.scrollTo({ top: 0, behavior: 'smooth' }); - } else { - // Error - errorMessage.textContent = data.message || 'An error occurred. Please try again.'; - errorMessage.style.display = 'block'; - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - - } catch (error) { - console.error('Submit error:', error); - errorMessage.textContent = 'Network error. Please check your connection and try again.'; - errorMessage.style.display = 'block'; - window.scrollTo({ top: 0, behavior: 'smooth' }); - } finally { - // Re-enable submit button - submitButton.disabled = false; - submitButton.textContent = 'Submit Inquiry'; - } -}); diff --git a/public/js/media-triage-transparency.js b/public/js/media-triage-transparency.js deleted file mode 100644 index 9a1be5d8..00000000 --- a/public/js/media-triage-transparency.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Media Triage Transparency - Public Statistics Display - * Demonstrates AI governance in practice with measurable transparency - */ - -// Fetch and display triage statistics -async function loadTriageStats() { - const loadingState = document.getElementById('loading-state'); - const statsContent = document.getElementById('stats-content'); - - try { - const response = await fetch('/api/media/triage-stats'); - const data = await response.json(); - - if (!data.success) { - throw new Error('Failed to load statistics'); - } - - const stats = data.statistics; - - // Hide loading, show content - loadingState.classList.add('hidden'); - statsContent.classList.remove('hidden'); - - // Update key metrics - document.getElementById('stat-total').textContent = stats.total_triaged || 0; - document.getElementById('stat-values').textContent = stats.involves_values_count || 0; - - // Update urgency distribution - const totalUrgency = stats.by_urgency.high + stats.by_urgency.medium + stats.by_urgency.low || 1; - - const highPct = Math.round((stats.by_urgency.high / totalUrgency) * 100); - const mediumPct = Math.round((stats.by_urgency.medium / totalUrgency) * 100); - const lowPct = Math.round((stats.by_urgency.low / totalUrgency) * 100); - - document.getElementById('urgency-high-count').textContent = `${stats.by_urgency.high} inquiries (${highPct}%)`; - document.getElementById('urgency-high-bar').style.width = `${highPct}%`; - - document.getElementById('urgency-medium-count').textContent = `${stats.by_urgency.medium} inquiries (${mediumPct}%)`; - document.getElementById('urgency-medium-bar').style.width = `${mediumPct}%`; - - document.getElementById('urgency-low-count').textContent = `${stats.by_urgency.low} inquiries (${lowPct}%)`; - document.getElementById('urgency-low-bar').style.width = `${lowPct}%`; - - // Update sensitivity distribution - const totalSensitivity = stats.by_sensitivity.high + stats.by_sensitivity.medium + stats.by_sensitivity.low || 1; - - const sensHighPct = Math.round((stats.by_sensitivity.high / totalSensitivity) * 100); - const sensMediumPct = Math.round((stats.by_sensitivity.medium / totalSensitivity) * 100); - const sensLowPct = Math.round((stats.by_sensitivity.low / totalSensitivity) * 100); - - document.getElementById('sensitivity-high-count').textContent = `${stats.by_sensitivity.high} inquiries (${sensHighPct}%)`; - document.getElementById('sensitivity-high-bar').style.width = `${sensHighPct}%`; - - document.getElementById('sensitivity-medium-count').textContent = `${stats.by_sensitivity.medium} inquiries (${sensMediumPct}%)`; - document.getElementById('sensitivity-medium-bar').style.width = `${sensMediumPct}%`; - - document.getElementById('sensitivity-low-count').textContent = `${stats.by_sensitivity.low} inquiries (${sensLowPct}%)`; - document.getElementById('sensitivity-low-bar').style.width = `${sensLowPct}%`; - - // Update framework compliance metrics - document.getElementById('boundary-enforcements').textContent = stats.boundary_enforcements || 0; - document.getElementById('avg-response-time').textContent = stats.avg_response_time_hours || 0; - - } catch (error) { - console.error('Failed to load triage statistics:', error); - - // Show error message - loadingState.innerHTML = ` -
- - - -

Failed to load statistics

-

Please try again later or contact support if the problem persists.

-
- `; - } -} - -// Load stats on page load -loadTriageStats(); diff --git a/public/js/page-transitions.js b/public/js/page-transitions.js deleted file mode 100644 index 2b26743d..00000000 --- a/public/js/page-transitions.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Page Transitions - * Tractatus Framework - Phase 3: Page Transitions - * - * Provides smooth fade transitions between pages for better UX - */ - -class PageTransitions { - constructor() { - this.transitionDuration = 300; // milliseconds - this.init(); - } - - init() { - // Add fade-in class to body on page load - document.body.classList.add('page-fade-in'); - - // Attach click handlers to internal links - this.attachLinkHandlers(); - - console.log('[PageTransitions] Initialized'); - } - - attachLinkHandlers() { - // Get all internal links (href starts with / or is relative) - const links = document.querySelectorAll('a[href^="/"], a[href^="./"], a[href^="../"]'); - - links.forEach(link => { - // Skip if link has target="_blank" or download attribute - if (link.getAttribute('target') === '_blank' || link.hasAttribute('download')) { - return; - } - - link.addEventListener('click', (e) => { - // Allow Ctrl/Cmd+click to open in new tab - if (e.ctrlKey || e.metaKey || e.shiftKey) { - return; - } - - // Skip if link has hash (same-page navigation) - const href = link.getAttribute('href'); - if (href && href.startsWith('#')) { - return; - } - - e.preventDefault(); - this.transitionToPage(link.href); - }); - }); - - console.log(`[PageTransitions] Attached handlers to ${links.length} links`); - } - - transitionToPage(url) { - // Remove fade-in, add fade-out - document.body.classList.remove('page-fade-in'); - document.body.classList.add('page-fade-out'); - - // Navigate after fade-out completes - setTimeout(() => { - window.location.href = url; - }, this.transitionDuration); - } -} - -// Initialize on DOM ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - new PageTransitions(); - }); -} else { - new PageTransitions(); -} - -// Export for module systems -if (typeof module !== 'undefined' && module.exports) { - module.exports = PageTransitions; -} diff --git a/public/js/researcher-page.js b/public/js/researcher-page.js deleted file mode 100644 index b7a182cd..00000000 --- a/public/js/researcher-page.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Researcher Page - Accordion Functionality - * Handles expandable/collapsible sections for research content - */ - -document.addEventListener('DOMContentLoaded', function() { - // Get all accordion buttons - const accordionButtons = document.querySelectorAll('[data-accordion]'); - - accordionButtons.forEach(button => { - button.addEventListener('click', function() { - const accordionId = this.dataset.accordion; - toggleAccordion(accordionId); - }); - }); - - /** - * Toggle accordion section open/closed - * @param {string} id - Accordion section ID - */ - function toggleAccordion(id) { - const content = document.getElementById(id + '-content'); - const icon = document.getElementById(id + '-icon'); - - if (content && icon) { - content.classList.toggle('active'); - icon.classList.toggle('active'); - } - } -}); diff --git a/public/js/scroll-animations.js b/public/js/scroll-animations.js deleted file mode 100644 index d6839575..00000000 --- a/public/js/scroll-animations.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Scroll Animations using Intersection Observer - * Tractatus Framework - Phase 3: Engagement & Interactive Features - * - * Provides smooth scroll-triggered animations for elements marked with .animate-on-scroll - * Supports multiple animation types via data-animation attribute - */ - -class ScrollAnimations { - constructor(options = {}) { - this.observerOptions = { - threshold: options.threshold || 0.1, - rootMargin: options.rootMargin || '0px 0px -100px 0px' - }; - - this.animations = { - 'fade-in': 'opacity-0 animate-fade-in', - 'slide-up': 'opacity-0 translate-y-8 animate-slide-up', - 'slide-down': 'opacity-0 -translate-y-8 animate-slide-down', - 'slide-left': 'opacity-0 translate-x-8 animate-slide-left', - 'slide-right': 'opacity-0 -translate-x-8 animate-slide-right', - 'scale-in': 'opacity-0 scale-95 animate-scale-in', - 'rotate-in': 'opacity-0 rotate-12 animate-rotate-in' - }; - - this.init(); - } - - init() { - // Wait for DOM to be ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => this.observe()); - } else { - this.observe(); - } - - console.log('[ScrollAnimations] Initialized'); - } - - observe() { - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - this.animateElement(entry.target); - - // Optional: unobserve after animation to improve performance - // Only do this for elements without data-animate-repeat attribute - if (!entry.target.hasAttribute('data-animate-repeat')) { - observer.unobserve(entry.target); - } - } else if (entry.target.hasAttribute('data-animate-repeat')) { - // Reset animation for repeatable elements - this.resetElement(entry.target); - } - }); - }, this.observerOptions); - - // Find all elements with .animate-on-scroll class - const elements = document.querySelectorAll('.animate-on-scroll'); - console.log(`[ScrollAnimations] Observing ${elements.length} elements`); - - elements.forEach((el, index) => { - // Add stagger delay if data-stagger attribute is present - if (el.hasAttribute('data-stagger')) { - const delay = parseInt(el.getAttribute('data-stagger')) || (index * 100); - el.style.animationDelay = `${delay}ms`; - } - - // Apply initial animation classes based on data-animation attribute - const animationType = el.getAttribute('data-animation') || 'fade-in'; - if (this.animations[animationType]) { - // Remove any existing animation classes - Object.values(this.animations).forEach(classes => { - classes.split(' ').forEach(cls => el.classList.remove(cls)); - }); - - // Add initial state classes (will be removed when visible) - const initialClasses = this.getInitialClasses(animationType); - initialClasses.forEach(cls => el.classList.add(cls)); - } - - observer.observe(el); - }); - } - - getInitialClasses(animationType) { - // Return classes that represent the "before animation" state - const map = { - 'fade-in': ['opacity-0'], - 'slide-up': ['opacity-0', 'translate-y-8'], - 'slide-down': ['opacity-0', '-translate-y-8'], - 'slide-left': ['opacity-0', 'translate-x-8'], - 'slide-right': ['opacity-0', '-translate-x-8'], - 'scale-in': ['opacity-0', 'scale-95'], - 'rotate-in': ['opacity-0', 'rotate-12'] - }; - - return map[animationType] || ['opacity-0']; - } - - animateElement(element) { - // Remove initial state classes - element.classList.remove('opacity-0', 'translate-y-8', '-translate-y-8', 'translate-x-8', '-translate-x-8', 'scale-95', 'rotate-12'); - - // Add visible state - element.classList.add('is-visible'); - - // Trigger custom event for other components to listen to - element.dispatchEvent(new CustomEvent('scroll-animated', { - bubbles: true, - detail: { element } - })); - } - - resetElement(element) { - // Remove visible state - element.classList.remove('is-visible'); - - // Re-apply initial animation classes - const animationType = element.getAttribute('data-animation') || 'fade-in'; - const initialClasses = this.getInitialClasses(animationType); - initialClasses.forEach(cls => element.classList.add(cls)); - } -} - -// Auto-initialize when script loads -// Can be disabled by setting window.DISABLE_AUTO_SCROLL_ANIMATIONS = true before loading this script -if (typeof window !== 'undefined' && !window.DISABLE_AUTO_SCROLL_ANIMATIONS) { - window.scrollAnimations = new ScrollAnimations(); -} - -// Export for module usage -if (typeof module !== 'undefined' && module.exports) { - module.exports = ScrollAnimations; -} diff --git a/public/js/utils/currency.js b/public/js/utils/currency.js deleted file mode 100644 index 081b81f7..00000000 --- a/public/js/utils/currency.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Currency Utilities (Client-Side) - * Multi-currency support for Koha donation form - */ - -// Base prices in NZD (in cents) -const BASE_PRICES_NZD = { - tier_5: 500, // $5 NZD - tier_15: 1500, // $15 NZD - tier_50: 5000 // $50 NZD -}; - -// Exchange rates: 1 NZD = X currency -const EXCHANGE_RATES = { - NZD: 1.0, - USD: 0.60, - EUR: 0.55, - GBP: 0.47, - AUD: 0.93, - CAD: 0.82, - JPY: 94.0, - CHF: 0.53, - SGD: 0.81, - HKD: 4.68 -}; - -// Currency metadata -const CURRENCY_CONFIG = { - NZD: { symbol: '$', code: 'NZD', name: 'NZ Dollar', decimals: 2, flag: '🇳🇿' }, - USD: { symbol: '$', code: 'USD', name: 'US Dollar', decimals: 2, flag: '🇺🇸' }, - EUR: { symbol: '€', code: 'EUR', name: 'Euro', decimals: 2, flag: '🇪🇺' }, - GBP: { symbol: '£', code: 'GBP', name: 'British Pound', decimals: 2, flag: '🇬🇧' }, - AUD: { symbol: '$', code: 'AUD', name: 'Australian Dollar', decimals: 2, flag: '🇦🇺' }, - CAD: { symbol: '$', code: 'CAD', name: 'Canadian Dollar', decimals: 2, flag: '🇨🇦' }, - JPY: { symbol: '¥', code: 'JPY', name: 'Japanese Yen', decimals: 0, flag: '🇯🇵' }, - CHF: { symbol: 'CHF', code: 'CHF', name: 'Swiss Franc', decimals: 2, flag: '🇨🇭' }, - SGD: { symbol: '$', code: 'SGD', name: 'Singapore Dollar', decimals: 2, flag: '🇸🇬' }, - HKD: { symbol: '$', code: 'HKD', name: 'Hong Kong Dollar', decimals: 2, flag: '🇭🇰' } -}; - -// Supported currencies -const SUPPORTED_CURRENCIES = ['NZD', 'USD', 'EUR', 'GBP', 'AUD', 'CAD', 'JPY', 'CHF', 'SGD', 'HKD']; - -/** - * Convert NZD amount to target currency - */ -function convertFromNZD(amountNZD, targetCurrency) { - const rate = EXCHANGE_RATES[targetCurrency]; - return Math.round(amountNZD * rate); -} - -/** - * Get tier prices for a currency - */ -function getTierPrices(currency) { - return { - tier_5: convertFromNZD(BASE_PRICES_NZD.tier_5, currency), - tier_15: convertFromNZD(BASE_PRICES_NZD.tier_15, currency), - tier_50: convertFromNZD(BASE_PRICES_NZD.tier_50, currency) - }; -} - -/** - * Format currency amount for display - */ -function formatCurrency(amountCents, currency) { - const config = CURRENCY_CONFIG[currency]; - const amount = amountCents / 100; - - // For currencies with symbols that should come after (none in our list currently) - // we could customize here, but Intl.NumberFormat handles it well - - try { - return new Intl.NumberFormat('en-NZ', { - style: 'currency', - currency: currency, - minimumFractionDigits: config.decimals, - maximumFractionDigits: config.decimals - }).format(amount); - } catch (e) { - // Fallback if Intl fails - return `${config.symbol}${amount.toFixed(config.decimals)}`; - } -} - -/** - * Get currency display name with flag - */ -function getCurrencyDisplayName(currency) { - const config = CURRENCY_CONFIG[currency]; - return `${config.flag} ${config.code} - ${config.name}`; -} - -/** - * Detect user's currency from browser/location - */ -function detectUserCurrency() { - // Try localStorage first - const saved = localStorage.getItem('tractatus_currency'); - if (saved && SUPPORTED_CURRENCIES.includes(saved)) { - return saved; - } - - // Try to detect from browser language - const lang = navigator.language || navigator.userLanguage || 'en-NZ'; - const langMap = { - 'en-US': 'USD', - 'en-GB': 'GBP', - 'en-AU': 'AUD', - 'en-CA': 'CAD', - 'en-NZ': 'NZD', - 'ja': 'JPY', - 'ja-JP': 'JPY', - 'de': 'EUR', - 'de-DE': 'EUR', - 'fr': 'EUR', - 'fr-FR': 'EUR', - 'de-CH': 'CHF', - 'en-SG': 'SGD', - 'zh-HK': 'HKD' - }; - - return langMap[lang] || langMap[lang.substring(0, 2)] || 'NZD'; -} - -/** - * Save user's currency preference - */ -function saveCurrencyPreference(currency) { - localStorage.setItem('tractatus_currency', currency); -} diff --git a/public/js/utils/router.js b/public/js/utils/router.js deleted file mode 100644 index 39f219d5..00000000 --- a/public/js/utils/router.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Simple client-side router for three audience paths - */ - -class Router { - constructor() { - this.routes = new Map(); - this.currentPath = null; - - // Initialize router - window.addEventListener('popstate', () => this.handleRoute()); - document.addEventListener('DOMContentLoaded', () => this.handleRoute()); - - // Handle link clicks - document.addEventListener('click', (e) => { - if (e.target.matches('[data-route]')) { - e.preventDefault(); - const path = e.target.getAttribute('data-route') || e.target.getAttribute('href'); - this.navigateTo(path); - } - }); - } - - /** - * Register a route - */ - on(path, handler) { - this.routes.set(path, handler); - return this; - } - - /** - * Navigate to a path - */ - navigateTo(path) { - if (path === this.currentPath) return; - - history.pushState(null, '', path); - this.handleRoute(); - } - - /** - * Handle current route - */ - async handleRoute() { - const path = window.location.pathname; - this.currentPath = path; - - // Try exact match - if (this.routes.has(path)) { - await this.routes.get(path)(); - return; - } - - // Try pattern match - for (const [pattern, handler] of this.routes) { - const match = this.matchRoute(pattern, path); - if (match) { - await handler(match.params); - return; - } - } - - // No match, show 404 - this.show404(); - } - - /** - * Match route pattern - */ - matchRoute(pattern, path) { - const patternParts = pattern.split('/'); - const pathParts = path.split('/'); - - if (patternParts.length !== pathParts.length) { - return null; - } - - const params = {}; - for (let i = 0; i < patternParts.length; i++) { - if (patternParts[i].startsWith(':')) { - const paramName = patternParts[i].slice(1); - params[paramName] = pathParts[i]; - } else if (patternParts[i] !== pathParts[i]) { - return null; - } - } - - return { params }; - } - - /** - * Show 404 page - */ - show404() { - const container = document.getElementById('app') || document.body; - container.innerHTML = ` -
- `; - } -} - -// Create global router instance -window.router = new Router(); diff --git a/public/js/version-manager.js b/public/js/version-manager.js deleted file mode 100644 index 79739cb3..00000000 --- a/public/js/version-manager.js +++ /dev/null @@ -1,421 +0,0 @@ -/** - * Tractatus Version Manager - * - Registers service worker - * - Checks for updates every hour - * - Shows update notifications - * - Manages PWA install prompts - */ - -class VersionManager { - constructor() { - this.serviceWorker = null; - this.deferredInstallPrompt = null; - this.updateCheckInterval = null; - this.currentVersion = null; - - this.init(); - } - - async init() { - // Only run in browsers that support service workers - if (!('serviceWorker' in navigator)) { - console.log('[VersionManager] Service workers not supported'); - return; - } - - try { - // Register service worker - await this.registerServiceWorker(); - - // Check for updates immediately - await this.checkForUpdates(); - - // Check for updates every hour - this.updateCheckInterval = setInterval(() => { - this.checkForUpdates(); - }, 3600000); // 1 hour - - // Listen for PWA install prompt - this.setupInstallPrompt(); - - // Listen for service worker messages - navigator.serviceWorker.addEventListener('message', (event) => { - if (event.data.type === 'UPDATE_AVAILABLE') { - this.showUpdateNotification(event.data); - } - }); - - } catch (error) { - console.error('[VersionManager] Initialization failed:', error); - } - } - - async registerServiceWorker() { - try { - const registration = await navigator.serviceWorker.register('/service-worker.js'); - this.serviceWorker = registration; - console.log('[VersionManager] Service worker registered'); - - // Check for updates when service worker updates - registration.addEventListener('updatefound', () => { - const newWorker = registration.installing; - newWorker.addEventListener('statechange', () => { - if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { - // New service worker available - this.showUpdateNotification({ - updateAvailable: true, - currentVersion: this.currentVersion, - serverVersion: 'latest' - }); - } - }); - }); - - } catch (error) { - console.error('[VersionManager] Service worker registration failed:', error); - } - } - - async checkForUpdates() { - try { - const response = await fetch('/version.json', { cache: 'no-store' }); - const versionInfo = await response.json(); - - // Get current version from localStorage or default - const storedVersion = localStorage.getItem('tractatus_version') || '0.0.0'; - this.currentVersion = storedVersion; - - if (storedVersion !== versionInfo.version) { - console.log('[VersionManager] Update available:', versionInfo.version); - this.showUpdateNotification({ - updateAvailable: true, - currentVersion: storedVersion, - serverVersion: versionInfo.version, - changelog: versionInfo.changelog, - forceUpdate: versionInfo.forceUpdate - }); - } - - } catch (error) { - console.error('[VersionManager] Version check failed:', error); - } - } - - showUpdateNotification(versionInfo) { - // Don't show if notification already visible - if (document.getElementById('tractatus-update-notification')) { - return; - } - - const notification = document.createElement('div'); - notification.id = 'tractatus-update-notification'; - notification.className = 'fixed bottom-0 left-0 right-0 bg-blue-600 text-white px-4 py-3 shadow-lg z-50 transform transition-transform duration-300 translate-y-full'; - notification.innerHTML = ` -
-
- - - -
-

Update Available

-

- A new version of Tractatus Framework is available - ${versionInfo.serverVersion ? `(v${versionInfo.serverVersion})` : ''} -

- ${versionInfo.changelog ? ` -
- What's new? -
    - ${versionInfo.changelog.map(item => `
  • ${item}
  • `).join('')} -
-
- ` : ''} -
-
-
- ${versionInfo.forceUpdate ? ` - - ` : ` - - - `} -
-
- `; - - document.body.appendChild(notification); - - // Add event listeners (CSP compliant) - const updateNowBtn = document.getElementById('update-now-btn'); - const updateLaterBtn = document.getElementById('update-later-btn'); - const updateReloadBtn = document.getElementById('update-reload-btn'); - - if (updateNowBtn) { - updateNowBtn.addEventListener('click', () => this.applyUpdate()); - } - if (updateLaterBtn) { - updateLaterBtn.addEventListener('click', () => this.dismissUpdate()); - } - if (updateReloadBtn) { - updateReloadBtn.addEventListener('click', () => this.applyUpdate()); - } - - // Animate in - setTimeout(() => { - notification.classList.remove('translate-y-full'); - }, 100); - - // Auto-reload for forced updates after 10 seconds - if (versionInfo.forceUpdate) { - setTimeout(() => { - this.applyUpdate(); - }, 10000); - } - } - - applyUpdate() { - // Store new version - fetch('/version.json', { cache: 'no-store' }) - .then(response => response.json()) - .then(versionInfo => { - localStorage.setItem('tractatus_version', versionInfo.version); - - // Tell service worker to skip waiting - if (this.serviceWorker) { - this.serviceWorker.waiting?.postMessage({ type: 'SKIP_WAITING' }); - } - - // Reload page - window.location.reload(); - }); - } - - dismissUpdate() { - const notification = document.getElementById('tractatus-update-notification'); - if (notification) { - notification.classList.add('translate-y-full'); - setTimeout(() => { - notification.remove(); - }, 300); - } - } - - setupInstallPrompt() { - // Listen for beforeinstallprompt event - window.addEventListener('beforeinstallprompt', (e) => { - // Prevent Chrome 67 and earlier from automatically showing the prompt - e.preventDefault(); - - // Stash the event so it can be triggered later - this.deferredInstallPrompt = e; - - // Check if user has dismissed install prompt before - const dismissed = sessionStorage.getItem('install_prompt_dismissed'); - if (!dismissed) { - // Show install prompt after 30 seconds - setTimeout(() => { - this.showInstallPrompt(); - }, 30000); - } - }); - - // Detect if app was installed - window.addEventListener('appinstalled', () => { - console.log('[VersionManager] PWA installed'); - this.deferredInstallPrompt = null; - - // Hide install prompt if visible - const prompt = document.getElementById('tractatus-install-prompt'); - if (prompt) { - prompt.remove(); - } - }); - } - - showInstallPrompt() { - if (!this.deferredInstallPrompt) { - return; - } - - // Don't show if already installed or on iOS Safari (handles differently) - if (window.matchMedia('(display-mode: standalone)').matches) { - return; - } - - const prompt = document.createElement('div'); - prompt.id = 'tractatus-install-prompt'; - prompt.className = 'fixed bottom-0 left-0 right-0 bg-gradient-to-r from-purple-600 to-blue-600 text-white px-4 py-3 shadow-lg z-50 transform transition-transform duration-300 translate-y-full'; - prompt.innerHTML = ` -
-
- - - -
-

Install Tractatus App

-

- Add to your home screen for quick access and offline support -

-
-
-
- - -
-
- `; - - document.body.appendChild(prompt); - - // Add event listeners (CSP compliant) - const dismissBtn = document.getElementById('dismiss-install-btn'); - const installBtn = document.getElementById('install-app-btn'); - - if (dismissBtn) { - dismissBtn.addEventListener('click', () => this.dismissInstallPrompt()); - } - if (installBtn) { - installBtn.addEventListener('click', () => this.installApp()); - } - - // Animate in - setTimeout(() => { - prompt.classList.remove('translate-y-full'); - }, 100); - } - - async installApp() { - if (!this.deferredInstallPrompt) { - // Show helpful feedback if installation isn't available - this.showInstallUnavailableMessage(); - return; - } - - // Show the install prompt - this.deferredInstallPrompt.prompt(); - - // Wait for the user to respond to the prompt - const { outcome } = await this.deferredInstallPrompt.userChoice; - console.log(`[VersionManager] User response: ${outcome}`); - - // Clear the deferredInstallPrompt - this.deferredInstallPrompt = null; - - // Hide the prompt - this.dismissInstallPrompt(); - } - - showInstallUnavailableMessage() { - // Check if app is already installed - const isInstalled = window.matchMedia('(display-mode: standalone)').matches; - - // Don't show message if it already exists - if (document.getElementById('tractatus-install-unavailable')) { - return; - } - - const message = document.createElement('div'); - message.id = 'tractatus-install-unavailable'; - message.className = 'fixed bottom-0 left-0 right-0 bg-gray-800 text-white px-4 py-3 shadow-lg z-50 transform transition-transform duration-300 translate-y-full'; - - if (isInstalled) { - message.innerHTML = ` -
-
- - - -
-

Already Installed

-

- Tractatus is already installed on your device. You're using it right now! -

-
-
- -
- `; - } else { - message.innerHTML = ` -
-
- - - -
-

Installation Not Available

-

- Your browser doesn't currently support app installation. Try using Chrome, Edge, or Safari on a supported device. -

-
-
- -
- `; - } - - document.body.appendChild(message); - - // Add event listener for dismiss button - const dismissBtn = document.getElementById('dismiss-unavailable-btn'); - if (dismissBtn) { - dismissBtn.addEventListener('click', () => { - message.classList.add('translate-y-full'); - setTimeout(() => { - message.remove(); - }, 300); - }); - } - - // Animate in - setTimeout(() => { - message.classList.remove('translate-y-full'); - }, 100); - - // Auto-dismiss after 8 seconds - setTimeout(() => { - if (message.parentElement) { - message.classList.add('translate-y-full'); - setTimeout(() => { - message.remove(); - }, 300); - } - }, 8000); - } - - dismissInstallPrompt() { - const prompt = document.getElementById('tractatus-install-prompt'); - if (prompt) { - prompt.classList.add('translate-y-full'); - setTimeout(() => { - prompt.remove(); - }, 300); - } - - // Remember dismissal for this session - sessionStorage.setItem('install_prompt_dismissed', 'true'); - } -} - -// Initialize version manager on page load -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => { - window.versionManager = new VersionManager(); - }); -} else { - window.versionManager = new VersionManager(); -} diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js deleted file mode 100644 index 61e6e731..00000000 --- a/src/controllers/admin.controller.js +++ /dev/null @@ -1,388 +0,0 @@ -/** - * Admin Controller - * Moderation queue and system statistics - */ - -const ModerationQueue = require('../models/ModerationQueue.model'); -const Document = require('../models/Document.model'); -const BlogPost = require('../models/BlogPost.model'); -const User = require('../models/User.model'); -const logger = require('../utils/logger.util'); - -/** - * Get moderation queue dashboard - * GET /api/admin/moderation - */ -async function getModerationQueue(req, res) { - try { - const { limit = 20, skip = 0, priority, quadrant, item_type, type } = req.query; - - let items; - let total; - - // Support both new 'type' and legacy 'item_type' fields - // Treat 'all' as no filter (same as not providing a type) - const filterType = (type && type !== 'all') ? type : (item_type && item_type !== 'all' ? item_type : null); - - if (quadrant) { - items = await ModerationQueue.findByQuadrant(quadrant, { - limit: parseInt(limit), - skip: parseInt(skip) - }); - total = await ModerationQueue.countPending({ quadrant }); - } else if (filterType) { - // Filter by new 'type' field (preferred) or legacy 'item_type' field - const collection = await require('../utils/db.util').getCollection('moderation_queue'); - items = await collection - .find({ - status: 'pending', - $or: [ - { type: filterType }, - { item_type: filterType } - ] - }) - .sort({ priority: -1, created_at: 1 }) - .skip(parseInt(skip)) - .limit(parseInt(limit)) - .toArray(); - - total = await collection.countDocuments({ - status: 'pending', - $or: [ - { type: filterType }, - { item_type: filterType } - ] - }); - } else { - items = await ModerationQueue.findPending({ - limit: parseInt(limit), - skip: parseInt(skip), - priority - }); - total = await ModerationQueue.countPending(priority ? { priority } : {}); - } - - // Get stats by quadrant - const stats = await ModerationQueue.getStatsByQuadrant(); - - res.json({ - success: true, - items, - queue: items, // Alias for backward compatibility - stats: stats.reduce((acc, stat) => { - acc[stat._id] = { - total: stat.count, - high_priority: stat.high_priority - }; - return acc; - }, {}), - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip) - } - }); - - } catch (error) { - logger.error('Get moderation queue error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get single moderation item - * GET /api/admin/moderation/:id - */ -async function getModerationItem(req, res) { - try { - const { id } = req.params; - - const item = await ModerationQueue.findById(id); - - if (!item) { - return res.status(404).json({ - error: 'Not Found', - message: 'Moderation item not found' - }); - } - - res.json({ - success: true, - item - }); - - } catch (error) { - logger.error('Get moderation item error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Review moderation item (approve/reject/escalate) - * POST /api/admin/moderation/:id/review - */ -async function reviewModerationItem(req, res) { - try { - const { id } = req.params; - const { action, notes } = req.body; - - const item = await ModerationQueue.findById(id); - - if (!item) { - return res.status(404).json({ - error: 'Not Found', - message: 'Moderation item not found' - }); - } - - let success; - let createdPost = null; - - switch (action) { - case 'approve': - success = await ModerationQueue.approve(id, req.userId, notes); - - // Blog-specific handling: Create BlogPost from approved draft - if (success && item.type === 'BLOG_POST_DRAFT' && item.data?.draft) { - try { - const draft = item.data.draft; - const slug = generateSlug(draft.title); - - // Create and publish blog post - createdPost = await BlogPost.create({ - title: draft.title, - slug, - content: draft.content, - excerpt: draft.excerpt, - tags: draft.tags || [], - author: { - type: 'ai_curated', - name: req.user.name || req.user.email, - claude_version: item.metadata?.model_info?.model || 'claude-3-5-sonnet' - }, - author_name: req.user.name || req.user.email, // Flattened for frontend - ai_assisted: true, // Flag for AI disclosure - tractatus_classification: { - quadrant: item.quadrant || 'OPERATIONAL', - values_sensitive: false, - requires_strategic_review: false - }, - moderation: { - ai_analysis: item.data.validation, - human_reviewer: req.userId, - review_notes: notes, - approved_at: new Date() - }, - status: 'published', - published_at: new Date() - }); - - // Log governance action - const GovernanceLog = require('../models/GovernanceLog.model'); - await GovernanceLog.create({ - action: 'BLOG_POST_PUBLISHED', - user_id: req.userId, - user_email: req.user.email, - timestamp: new Date(), - outcome: 'APPROVED', - details: { - post_id: createdPost._id, - slug, - title: draft.title, - queue_id: id, - validation_result: item.data.validation?.recommendation - } - }); - - logger.info(`Blog post created from approved draft: ${createdPost._id} (${slug}) by ${req.user.email}`); - } catch (blogError) { - logger.error('Failed to create blog post from approved draft:', blogError); - // Don't fail the entire approval if blog creation fails - } - } - break; - case 'reject': - success = await ModerationQueue.reject(id, req.userId, notes); - break; - case 'escalate': - success = await ModerationQueue.escalate(id, req.userId, notes); - break; - default: - return res.status(400).json({ - error: 'Bad Request', - message: 'Invalid action. Must be: approve, reject, or escalate' - }); - } - - if (!success) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to update moderation item' - }); - } - - const updatedItem = await ModerationQueue.findById(id); - - logger.info(`Moderation item ${action}: ${id} by ${req.user.email}`); - - res.json({ - success: true, - item: updatedItem, - message: `Item ${action}d successfully`, - blog_post: createdPost ? { - id: createdPost._id, - slug: createdPost.slug, - title: createdPost.title, - url: `/blog-post.html?slug=${createdPost.slug}` - } : undefined - }); - - } catch (error) { - logger.error('Review moderation item error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Generate URL-friendly slug from title - */ -function generateSlug(title) { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, '') - .substring(0, 100); -} - -/** - * Get system statistics - * GET /api/admin/stats - */ -async function getSystemStats(req, res) { - try { - // Document stats - const totalDocuments = await Document.count(); - const documentsByQuadrant = await Promise.all([ - Document.count({ quadrant: 'STRATEGIC' }), - Document.count({ quadrant: 'OPERATIONAL' }), - Document.count({ quadrant: 'TACTICAL' }), - Document.count({ quadrant: 'SYSTEM' }), - Document.count({ quadrant: 'STOCHASTIC' }) - ]); - - // Blog stats - const blogStats = await Promise.all([ - BlogPost.countByStatus('published'), - BlogPost.countByStatus('draft'), - BlogPost.countByStatus('pending') - ]); - - // Moderation queue stats - const moderationStats = await ModerationQueue.getStatsByQuadrant(); - const totalPending = await ModerationQueue.countPending(); - - // User stats - const totalUsers = await User.count(); - const activeUsers = await User.count({ active: true }); - - res.json({ - success: true, - stats: { - documents: { - total: totalDocuments, - by_quadrant: { - STRATEGIC: documentsByQuadrant[0], - OPERATIONAL: documentsByQuadrant[1], - TACTICAL: documentsByQuadrant[2], - SYSTEM: documentsByQuadrant[3], - STOCHASTIC: documentsByQuadrant[4] - } - }, - blog: { - published: blogStats[0], - draft: blogStats[1], - pending: blogStats[2], - total: blogStats[0] + blogStats[1] + blogStats[2] - }, - moderation: { - total_pending: totalPending, - by_quadrant: moderationStats.reduce((acc, stat) => { - acc[stat._id] = { - total: stat.count, - high_priority: stat.high_priority - }; - return acc; - }, {}) - }, - users: { - total: totalUsers, - active: activeUsers, - inactive: totalUsers - activeUsers - } - } - }); - - } catch (error) { - logger.error('Get system stats error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get recent activity log - * GET /api/admin/activity - */ -async function getActivityLog(req, res) { - try { - // This would typically read from a dedicated activity log - // For now, return recent moderation reviews as example - const { limit = 50 } = req.query; - - const collection = await require('../utils/db.util').getCollection('moderation_queue'); - - const recentActivity = await collection - .find({ status: 'reviewed' }) - .sort({ reviewed_at: -1 }) - .limit(parseInt(limit)) - .toArray(); - - res.json({ - success: true, - activity: recentActivity.map(item => ({ - timestamp: item.reviewed_at, - action: item.review_decision?.action, - item_type: item.item_type, - item_id: item.item_id, - reviewer: item.review_decision?.reviewer, - notes: item.review_decision?.notes - })) - }); - - } catch (error) { - logger.error('Get activity log error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -module.exports = { - getModerationQueue, - getModerationItem, - reviewModerationItem, - getSystemStats, - getActivityLog -}; diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js deleted file mode 100644 index b372d958..00000000 --- a/src/controllers/auth.controller.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Authentication Controller - * Handles user login and token verification - */ - -const User = require('../models/User.model'); -const { generateToken } = require('../utils/jwt.util'); -const logger = require('../utils/logger.util'); - -/** - * Login user - * POST /api/auth/login - */ -async function login(req, res) { - try { - const { email, password } = req.body; - - // Authenticate user - const user = await User.authenticate(email, password); - - if (!user) { - logger.warn(`Failed login attempt for email: ${email}`); - return res.status(401).json({ - error: 'Authentication failed', - message: 'Invalid email or password' - }); - } - - // Generate JWT token - const token = generateToken({ - userId: user._id.toString(), - email: user.email, - role: user.role - }); - - logger.info(`User logged in: ${user.email}`); - - res.json({ - success: true, - accessToken: token, - user: { - id: user._id.toString(), - email: user.email, - name: user.name, - role: user.role - } - }); - - } catch (error) { - logger.error('Login error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred during login' - }); - } -} - -/** - * Verify token and get current user - * GET /api/auth/me - */ -async function getCurrentUser(req, res) { - try { - // User is already attached to req by auth middleware - const user = await User.findById(req.userId); - - if (!user) { - return res.status(404).json({ - error: 'Not Found', - message: 'User not found' - }); - } - - res.json({ - success: true, - user: { - id: user._id, - email: user.email, - name: user.name, - role: user.role, - created_at: user.created_at, - last_login: user.last_login - } - }); - - } catch (error) { - logger.error('Get current user error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Logout (client-side token removal, server logs it) - * POST /api/auth/logout - */ -async function logout(req, res) { - try { - logger.info(`User logged out: ${req.user.email}`); - - res.json({ - success: true, - message: 'Logged out successfully' - }); - - } catch (error) { - logger.error('Logout error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -module.exports = { - login, - getCurrentUser, - logout -}; diff --git a/src/controllers/blog.controller.js b/src/controllers/blog.controller.js deleted file mode 100644 index 2f695b9e..00000000 --- a/src/controllers/blog.controller.js +++ /dev/null @@ -1,764 +0,0 @@ -/** - * Blog Controller - * AI-curated blog with human oversight - */ - -const BlogPost = require('../models/BlogPost.model'); -const ModerationQueue = require('../models/ModerationQueue.model'); -const GovernanceLog = require('../models/GovernanceLog.model'); -const { markdownToHtml } = require('../utils/markdown.util'); -const logger = require('../utils/logger.util'); -const claudeAPI = require('../services/ClaudeAPI.service'); -const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); -const BlogCuration = require('../services/BlogCuration.service'); - -/** - * List published blog posts (public) - * GET /api/blog - */ -async function listPublishedPosts(req, res) { - try { - const { limit = 10, skip = 0 } = req.query; - - const posts = await BlogPost.findPublished({ - limit: parseInt(limit), - skip: parseInt(skip) - }); - - const total = await BlogPost.countByStatus('published'); - - res.json({ - success: true, - posts, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip), - hasMore: parseInt(skip) + posts.length < total - } - }); - - } catch (error) { - logger.error('List published posts error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get single blog post by slug (public, published only) - * GET /api/blog/:slug - */ -async function getPublishedPost(req, res) { - try { - const { slug } = req.params; - - const post = await BlogPost.findBySlug(slug); - - if (!post || post.status !== 'published') { - return res.status(404).json({ - error: 'Not Found', - message: 'Blog post not found' - }); - } - - // Increment view count - await BlogPost.incrementViews(post._id); - - res.json({ - success: true, - post - }); - - } catch (error) { - logger.error('Get published post error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * List posts by status (admin only) - * GET /api/blog/admin/posts?status=draft - */ -async function listPostsByStatus(req, res) { - try { - const { status = 'draft', limit = 20, skip = 0 } = req.query; - - const posts = await BlogPost.findByStatus(status, { - limit: parseInt(limit), - skip: parseInt(skip) - }); - - const total = await BlogPost.countByStatus(status); - - res.json({ - success: true, - status, - posts, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip) - } - }); - - } catch (error) { - logger.error('List posts by status error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get any post by ID (admin only) - * GET /api/blog/admin/:id - */ -async function getPostById(req, res) { - try { - const { id } = req.params; - - const post = await BlogPost.findById(id); - - if (!post) { - return res.status(404).json({ - error: 'Not Found', - message: 'Blog post not found' - }); - } - - res.json({ - success: true, - post - }); - - } catch (error) { - logger.error('Get post by ID error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Create blog post (admin only) - * POST /api/blog - */ -async function createPost(req, res) { - try { - const { title, slug, content, excerpt, tags, author, tractatus_classification } = req.body; - - // Convert markdown content to HTML if needed - const content_html = content.includes('# ') ? markdownToHtml(content) : content; - - const post = await BlogPost.create({ - title, - slug, - content: content_html, - excerpt, - tags, - author: { - ...author, - name: author?.name || req.user.name || req.user.email - }, - tractatus_classification, - status: 'draft' - }); - - logger.info(`Blog post created: ${slug} by ${req.user.email}`); - - res.status(201).json({ - success: true, - post - }); - - } catch (error) { - logger.error('Create post error:', error); - - // Handle duplicate slug - if (error.code === 11000) { - return res.status(409).json({ - error: 'Conflict', - message: 'A post with this slug already exists' - }); - } - - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Update blog post (admin only) - * PUT /api/blog/:id - */ -async function updatePost(req, res) { - try { - const { id } = req.params; - const updates = { ...req.body }; - - // If content is updated and looks like markdown, convert to HTML - if (updates.content && updates.content.includes('# ')) { - updates.content = markdownToHtml(updates.content); - } - - const success = await BlogPost.update(id, updates); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Blog post not found' - }); - } - - const post = await BlogPost.findById(id); - - logger.info(`Blog post updated: ${id} by ${req.user.email}`); - - res.json({ - success: true, - post - }); - - } catch (error) { - logger.error('Update post error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Publish blog post (admin only) - * POST /api/blog/:id/publish - */ -async function publishPost(req, res) { - try { - const { id } = req.params; - const { review_notes } = req.body; - - const post = await BlogPost.findById(id); - - if (!post) { - return res.status(404).json({ - error: 'Not Found', - message: 'Blog post not found' - }); - } - - if (post.status === 'published') { - return res.status(400).json({ - error: 'Bad Request', - message: 'Post is already published' - }); - } - - // Update with review notes if provided - if (review_notes) { - await BlogPost.update(id, { - 'moderation.review_notes': review_notes - }); - } - - // Publish the post - await BlogPost.publish(id, req.userId); - - const updatedPost = await BlogPost.findById(id); - - logger.info(`Blog post published: ${id} by ${req.user.email}`); - - res.json({ - success: true, - post: updatedPost, - message: 'Post published successfully' - }); - - } catch (error) { - logger.error('Publish post error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Delete blog post (admin only) - * DELETE /api/blog/:id - */ -async function deletePost(req, res) { - try { - const { id } = req.params; - - const success = await BlogPost.delete(id); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Blog post not found' - }); - } - - logger.info(`Blog post deleted: ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Post deleted successfully' - }); - - } catch (error) { - logger.error('Delete post error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Suggest blog topics using AI (admin only) - * POST /api/blog/suggest-topics - * - * TRA-OPS-0002: AI suggests topics, human writes and approves posts - */ -async function suggestTopics(req, res) { - try { - const { audience, theme } = req.body; - - // Validate audience - const validAudiences = ['researcher', 'implementer', 'advocate', 'general']; - if (!audience || !validAudiences.includes(audience)) { - return res.status(400).json({ - error: 'Bad Request', - message: `Audience must be one of: ${validAudiences.join(', ')}` - }); - } - - logger.info(`Blog topic suggestion requested: audience=${audience}, theme=${theme || 'none'}`); - - // 1. Boundary check (TRA-OPS-0002: Editorial decisions require human oversight) - const boundaryCheck = BoundaryEnforcer.enforce({ - description: 'Suggest blog topics for editorial calendar', - text: 'AI provides suggestions, human makes final editorial decisions', - classification: { quadrant: 'OPERATIONAL' }, - type: 'content_suggestion' - }); - - // Log boundary check - await GovernanceLog.create({ - action: 'BLOG_TOPIC_SUGGESTION', - user_id: req.user._id, - user_email: req.user.email, - timestamp: new Date(), - boundary_check: boundaryCheck, - outcome: boundaryCheck.allowed ? 'QUEUED_FOR_APPROVAL' : 'BLOCKED', - details: { - audience, - theme - } - }); - - if (!boundaryCheck.allowed) { - logger.warn(`Blog topic suggestion blocked by BoundaryEnforcer: ${boundaryCheck.section}`); - return res.status(403).json({ - error: 'Boundary Violation', - message: boundaryCheck.reasoning, - section: boundaryCheck.section, - details: 'This action requires human judgment in values territory' - }); - } - - // 2. Claude API call for topic suggestions - const suggestions = await claudeAPI.generateBlogTopics(audience, theme); - - logger.info(`Claude API returned ${suggestions.length} topic suggestions`); - - // 3. Create moderation queue entry (human approval required) - const queueEntry = await ModerationQueue.create({ - type: 'BLOG_TOPIC_SUGGESTION', - reference_collection: 'blog_posts', - data: { - audience, - theme, - suggestions, - requested_by: req.user.email - }, - status: 'PENDING_APPROVAL', - ai_generated: true, - requires_human_approval: true, - created_by: req.user._id, - created_at: new Date(), - metadata: { - boundary_check: boundaryCheck, - governance_policy: 'TRA-OPS-0002' - } - }); - - logger.info(`Created moderation queue entry: ${queueEntry._id}`); - - // 4. Return response (suggestions queued for human review) - res.json({ - success: true, - message: 'Blog topic suggestions generated. Awaiting human review and approval.', - queue_id: queueEntry._id, - suggestions, - governance: { - policy: 'TRA-OPS-0002', - boundary_check: boundaryCheck, - requires_approval: true, - note: 'Topics are suggestions only. Human must write all blog posts.' - } - }); - - } catch (error) { - logger.error('Suggest topics error:', error); - - // Handle Claude API errors specifically - if (error.message.includes('Claude API')) { - return res.status(502).json({ - error: 'AI Service Error', - message: 'Failed to generate topic suggestions. Please try again.', - details: process.env.NODE_ENV === 'development' ? error.message : undefined - }); - } - - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Draft a full blog post using AI (admin only) - * POST /api/blog/draft-post - * - * TRA-OPS-0002: AI drafts content, human reviews and approves before publication - * Enforces inst_016, inst_017, inst_018 via BlogCuration service - */ -async function draftBlogPost(req, res) { - try { - const { topic, audience, length = 'medium', focus } = req.body; - - // Validate required fields - if (!topic || !audience) { - return res.status(400).json({ - error: 'Bad Request', - message: 'topic and audience are required' - }); - } - - const validAudiences = ['researcher', 'implementer', 'advocate', 'general']; - if (!validAudiences.includes(audience)) { - return res.status(400).json({ - error: 'Bad Request', - message: `audience must be one of: ${validAudiences.join(', ')}` - }); - } - - const validLengths = ['short', 'medium', 'long']; - if (!validLengths.includes(length)) { - return res.status(400).json({ - error: 'Bad Request', - message: `length must be one of: ${validLengths.join(', ')}` - }); - } - - logger.info(`Blog post draft requested: topic="${topic}", audience=${audience}, length=${length}`); - - // Generate draft using BlogCuration service (includes boundary checks and validation) - const result = await BlogCuration.draftBlogPost({ - topic, - audience, - length, - focus - }); - - const { draft, validation, boundary_check, metadata } = result; - - // Log governance action - await GovernanceLog.create({ - action: 'BLOG_POST_DRAFT', - user_id: req.user._id, - user_email: req.user.email, - timestamp: new Date(), - boundary_check, - outcome: 'QUEUED_FOR_APPROVAL', - details: { - topic, - audience, - length, - validation_result: validation.recommendation, - violations: validation.violations.length, - warnings: validation.warnings.length - } - }); - - // Create moderation queue entry (human approval required) - const queueEntry = await ModerationQueue.create({ - type: 'BLOG_POST_DRAFT', - reference_collection: 'blog_posts', - data: { - topic, - audience, - length, - focus, - draft, - validation, - requested_by: req.user.email - }, - status: 'PENDING_APPROVAL', - ai_generated: true, - requires_human_approval: true, - created_by: req.user._id, - created_at: new Date(), - priority: validation.violations.length > 0 ? 'high' : 'medium', - metadata: { - boundary_check, - governance_policy: 'TRA-OPS-0002', - tractatus_instructions: ['inst_016', 'inst_017', 'inst_018'], - model_info: metadata - } - }); - - logger.info(`Created blog draft queue entry: ${queueEntry._id}, validation: ${validation.recommendation}`); - - // Return response - res.json({ - success: true, - message: 'Blog post draft generated. Awaiting human review and approval.', - queue_id: queueEntry._id, - draft, - validation, - governance: { - policy: 'TRA-OPS-0002', - boundary_check, - requires_approval: true, - tractatus_enforcement: { - inst_016: 'No fabricated statistics or unverifiable claims', - inst_017: 'No absolute assurance terms (guarantee, 100%, etc.)', - inst_018: 'No unverified production-ready claims' - } - } - }); - - } catch (error) { - logger.error('Draft blog post error:', error); - - // Handle boundary violations - if (error.message.includes('Boundary violation')) { - return res.status(403).json({ - error: 'Boundary Violation', - message: error.message - }); - } - - // Handle Claude API errors - if (error.message.includes('Claude API') || error.message.includes('Blog draft generation failed')) { - return res.status(502).json({ - error: 'AI Service Error', - message: 'Failed to generate blog draft. Please try again.', - details: process.env.NODE_ENV === 'development' ? error.message : undefined - }); - } - - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Analyze blog content for Tractatus compliance (admin only) - * POST /api/blog/analyze-content - * - * Validates content against inst_016, inst_017, inst_018 - */ -async function analyzeContent(req, res) { - try { - const { title, body } = req.body; - - if (!title || !body) { - return res.status(400).json({ - error: 'Bad Request', - message: 'title and body are required' - }); - } - - logger.info(`Content compliance analysis requested: "${title}"`); - - const analysis = await BlogCuration.analyzeContentCompliance({ - title, - body - }); - - logger.info(`Compliance analysis complete: ${analysis.recommendation}, score: ${analysis.overall_score}`); - - res.json({ - success: true, - analysis, - tractatus_enforcement: { - inst_016: 'No fabricated statistics', - inst_017: 'No absolute guarantees', - inst_018: 'No unverified production claims' - } - }); - - } catch (error) { - logger.error('Analyze content error:', error); - - if (error.message.includes('Claude API') || error.message.includes('Compliance analysis failed')) { - return res.status(502).json({ - error: 'AI Service Error', - message: 'Failed to analyze content. Please try again.', - details: process.env.NODE_ENV === 'development' ? error.message : undefined - }); - } - - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get editorial guidelines (admin only) - * GET /api/blog/editorial-guidelines - */ -async function getEditorialGuidelines(req, res) { - try { - const guidelines = BlogCuration.getEditorialGuidelines(); - - res.json({ - success: true, - guidelines - }); - - } catch (error) { - logger.error('Get editorial guidelines error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Generate RSS feed for published blog posts (public) - * GET /api/blog/rss - */ -async function generateRSSFeed(req, res) { - try { - // Fetch recent published posts (limit to 50 most recent) - const posts = await BlogPost.findPublished({ - limit: 50, - skip: 0 - }); - - // RSS 2.0 feed structure - const baseUrl = process.env.FRONTEND_URL || 'https://agenticgovernance.digital'; - const buildDate = new Date().toUTCString(); - - // Start RSS XML - let rss = ` - - - Tractatus AI Safety Framework Blog - ${baseUrl}/blog.html - Insights, updates, and analysis on AI governance, safety frameworks, and the Tractatus boundary enforcement approach. - en-us - ${buildDate} - - - ${baseUrl}/images/tractatus-icon.svg - Tractatus AI Safety Framework - ${baseUrl}/blog.html - -`; - - // Add items for each post - for (const post of posts) { - const postUrl = `${baseUrl}/blog-post.html?slug=${post.slug}`; - const pubDate = new Date(post.published_at || post.created_at).toUTCString(); - const author = post.author_name || post.author?.name || 'Tractatus Team'; - - // Strip HTML tags from excerpt for RSS description - const description = (post.excerpt || post.content) - .replace(/<[^>]*>/g, '') - .substring(0, 500); - - // Tags as categories - const categories = (post.tags || []).map(tag => - ` ${escapeXml(tag)}` - ).join('\n'); - - rss += ` - ${escapeXml(post.title)} - ${postUrl} - ${postUrl} - ${escapeXml(description)} - ${escapeXml(author)} - ${pubDate} -${categories ? categories + '\n' : ''} -`; - } - - // Close RSS XML - rss += ` -`; - - // Set RSS content-type and send - res.set('Content-Type', 'application/rss+xml; charset=UTF-8'); - res.send(rss); - - logger.info(`RSS feed generated: ${posts.length} posts`); - - } catch (error) { - logger.error('Generate RSS feed error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to generate RSS feed' - }); - } -} - -/** - * Helper: Escape XML special characters - */ -function escapeXml(unsafe) { - if (!unsafe) return ''; - return unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -module.exports = { - listPublishedPosts, - getPublishedPost, - listPostsByStatus, - getPostById, - createPost, - updatePost, - publishPost, - deletePost, - suggestTopics, - draftBlogPost, - analyzeContent, - getEditorialGuidelines, - generateRSSFeed -}; diff --git a/src/controllers/cases.controller.js b/src/controllers/cases.controller.js deleted file mode 100644 index 27b1f133..00000000 --- a/src/controllers/cases.controller.js +++ /dev/null @@ -1,453 +0,0 @@ -/** - * Case Study Controller - * Community case study submissions with AI review - */ - -const CaseSubmission = require('../models/CaseSubmission.model'); -const ModerationQueue = require('../models/ModerationQueue.model'); -const GovernanceLog = require('../models/GovernanceLog.model'); -const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); -const logger = require('../utils/logger.util'); - -/** - * Submit case study (public) - * POST /api/cases/submit - * - * Phase 1: Manual review (no AI) - * Phase 2: Add AI categorization with claudeAPI.reviewCaseStudy() - */ -async function submitCase(req, res) { - try { - const { submitter, case_study } = req.body; - - // Validate required fields - if (!submitter?.name || !submitter?.email) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Missing required submitter information' - }); - } - - if (!case_study?.title || !case_study?.description || !case_study?.failure_mode) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Missing required case study information' - }); - } - - logger.info(`Case study submitted: ${case_study.title} by ${submitter.name}`); - - // Create submission (Phase 1: no AI review yet) - const submission = await CaseSubmission.create({ - submitter, - case_study, - ai_review: { - relevance_score: 0.5, // Default, will be AI-assessed in Phase 2 - completeness_score: 0.5, - recommended_category: 'uncategorized' - }, - moderation: { - status: 'pending' - } - }); - - // Add to moderation queue for human review - await ModerationQueue.create({ - type: 'CASE_SUBMISSION', - reference_collection: 'case_submissions', - reference_id: submission._id, - quadrant: 'OPERATIONAL', - data: { - submitter, - case_study - }, - priority: 'medium', - status: 'PENDING_APPROVAL', - requires_human_approval: true, - human_required_reason: 'All case submissions require human review and approval' - }); - - logger.info(`Case submission created: ${submission._id}`); - - res.status(201).json({ - success: true, - message: 'Thank you for your submission. We will review it shortly.', - submission_id: submission._id, - governance: { - human_review: true, - note: 'All case studies are reviewed by humans before publication' - } - }); - - } catch (error) { - logger.error('Submit case error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred while submitting your case study' - }); - } -} - -/** - * Get case submission statistics (admin) - * GET /api/cases/submissions/stats - */ -async function getStats(req, res) { - try { - const total = await CaseSubmission.countDocuments({}); - const pending = await CaseSubmission.countDocuments({ 'moderation.status': 'pending' }); - const approved = await CaseSubmission.countDocuments({ 'moderation.status': 'approved' }); - const rejected = await CaseSubmission.countDocuments({ 'moderation.status': 'rejected' }); - const needsInfo = await CaseSubmission.countDocuments({ 'moderation.status': 'needs_info' }); - - res.json({ - success: true, - stats: { - total, - pending, - approved, - rejected, - needs_info: needsInfo - } - }); - - } catch (error) { - logger.error('Get stats error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * List all case submissions (admin) - * GET /api/cases/submissions?status=pending&failure_mode=pattern_bias&score=high&sort=relevance_score - */ -async function listSubmissions(req, res) { - try { - const { - status, - failure_mode, - score, - sort = 'submitted_at', - limit = 20, - skip = 0 - } = req.query; - - // Build query filter - const filter = {}; - - if (status) { - filter['moderation.status'] = status; - } - - if (failure_mode) { - filter['case_study.failure_mode'] = failure_mode; - } - - // AI score filtering - if (score) { - if (score === 'high') { - filter['ai_review.relevance_score'] = { $gte: 0.7 }; - } else if (score === 'medium') { - filter['ai_review.relevance_score'] = { $gte: 0.4, $lt: 0.7 }; - } else if (score === 'low') { - filter['ai_review.relevance_score'] = { $lt: 0.4 }; - } - } - - // Build sort options - const sortOptions = {}; - if (sort === 'submitted_at') { - sortOptions.submitted_at = -1; // Newest first - } else if (sort === 'relevance_score') { - sortOptions['ai_review.relevance_score'] = -1; // Highest first - } else if (sort === 'completeness_score') { - sortOptions['ai_review.completeness_score'] = -1; // Highest first - } - - // Query database - const submissions = await CaseSubmission.find(filter) - .sort(sortOptions) - .limit(parseInt(limit)) - .skip(parseInt(skip)) - .lean(); - - const total = await CaseSubmission.countDocuments(filter); - - res.json({ - success: true, - submissions, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip), - hasMore: parseInt(skip) + submissions.length < total - } - }); - - } catch (error) { - logger.error('List submissions error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * List high-relevance pending submissions (admin) - * GET /api/cases/submissions/high-relevance - */ -async function listHighRelevance(req, res) { - try { - const { limit = 10 } = req.query; - - const submissions = await CaseSubmission.findHighRelevance({ - limit: parseInt(limit) - }); - - res.json({ - success: true, - count: submissions.length, - submissions - }); - - } catch (error) { - logger.error('List high relevance error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get case submission by ID (admin) - * GET /api/cases/submissions/:id - */ -async function getSubmission(req, res) { - try { - const { id } = req.params; - - const submission = await CaseSubmission.findById(id); - - if (!submission) { - return res.status(404).json({ - error: 'Not Found', - message: 'Case submission not found' - }); - } - - res.json({ - success: true, - submission - }); - - } catch (error) { - logger.error('Get submission error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Approve case submission (admin) - * POST /api/cases/submissions/:id/approve - */ -async function approveSubmission(req, res) { - try { - const { id } = req.params; - const { notes } = req.body; - - const submission = await CaseSubmission.findById(id); - - if (!submission) { - return res.status(404).json({ - error: 'Not Found', - message: 'Case submission not found' - }); - } - - if (submission.moderation.status === 'approved') { - return res.status(400).json({ - error: 'Bad Request', - message: 'Submission is already approved' - }); - } - - const success = await CaseSubmission.approve(id, req.user._id, notes || ''); - - if (!success) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to approve submission' - }); - } - - logger.info(`Case submission approved: ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Case submission approved successfully', - note: 'You can now publish this as a case study document' - }); - - } catch (error) { - logger.error('Approve submission error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Reject case submission (admin) - * POST /api/cases/submissions/:id/reject - */ -async function rejectSubmission(req, res) { - try { - const { id } = req.params; - const { reason } = req.body; - - if (!reason) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Rejection reason is required' - }); - } - - const submission = await CaseSubmission.findById(id); - - if (!submission) { - return res.status(404).json({ - error: 'Not Found', - message: 'Case submission not found' - }); - } - - const success = await CaseSubmission.reject(id, req.user._id, reason); - - if (!success) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to reject submission' - }); - } - - logger.info(`Case submission rejected: ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Case submission rejected', - note: 'Consider notifying the submitter with feedback' - }); - - } catch (error) { - logger.error('Reject submission error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Request more information (admin) - * POST /api/cases/submissions/:id/request-info - */ -async function requestMoreInfo(req, res) { - try { - const { id } = req.params; - const { requested_info } = req.body; - - if (!requested_info) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Requested information must be specified' - }); - } - - const submission = await CaseSubmission.findById(id); - - if (!submission) { - return res.status(404).json({ - error: 'Not Found', - message: 'Case submission not found' - }); - } - - const success = await CaseSubmission.requestInfo(id, req.user._id, requested_info); - - if (!success) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to update submission' - }); - } - - logger.info(`More info requested for case ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Information request recorded', - note: 'Remember to contact submitter separately to request additional information' - }); - - } catch (error) { - logger.error('Request info error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Delete case submission (admin) - * DELETE /api/cases/submissions/:id - */ -async function deleteSubmission(req, res) { - try { - const { id } = req.params; - - const success = await CaseSubmission.delete(id); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Case submission not found' - }); - } - - logger.info(`Case submission deleted: ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Case submission deleted successfully' - }); - - } catch (error) { - logger.error('Delete submission error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -module.exports = { - submitCase, - getStats, - listSubmissions, - listHighRelevance, - getSubmission, - approveSubmission, - rejectSubmission, - requestMoreInfo, - deleteSubmission -}; diff --git a/src/controllers/documents.controller.js b/src/controllers/documents.controller.js deleted file mode 100644 index f74c08ef..00000000 --- a/src/controllers/documents.controller.js +++ /dev/null @@ -1,480 +0,0 @@ -/** - * Documents Controller - * Handles framework documentation CRUD operations - */ - -const Document = require('../models/Document.model'); -const { markdownToHtml, extractTOC } = require('../utils/markdown.util'); -const logger = require('../utils/logger.util'); - -/** - * List all documents - * GET /api/documents - */ -async function listDocuments(req, res) { - try { - const { limit = 50, skip = 0, quadrant, audience } = req.query; - - let documents; - let total; - - // Build filter - only show public documents (not internal/confidential) - const filter = { - visibility: 'public' - }; - if (quadrant) { - filter.quadrant = quadrant; - } - if (audience) { - filter.audience = audience; - } - - if (quadrant && !audience) { - documents = await Document.findByQuadrant(quadrant, { - limit: parseInt(limit), - skip: parseInt(skip), - publicOnly: true - }); - total = await Document.count(filter); - } else if (audience && !quadrant) { - documents = await Document.findByAudience(audience, { - limit: parseInt(limit), - skip: parseInt(skip), - publicOnly: true - }); - total = await Document.count(filter); - } else { - documents = await Document.list({ - limit: parseInt(limit), - skip: parseInt(skip), - filter - }); - total = await Document.count(filter); - } - - res.json({ - success: true, - documents, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip), - hasMore: parseInt(skip) + documents.length < total - } - }); - - } catch (error) { - logger.error('List documents error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get document by ID or slug - * GET /api/documents/:identifier - */ -async function getDocument(req, res) { - try { - const { identifier } = req.params; - - // Try to find by ID first, then by slug - let document; - if (identifier.match(/^[0-9a-fA-F]{24}$/)) { - document = await Document.findById(identifier); - } else { - document = await Document.findBySlug(identifier); - } - - if (!document) { - return res.status(404).json({ - error: 'Not Found', - message: 'Document not found' - }); - } - - res.json({ - success: true, - document - }); - - } catch (error) { - logger.error('Get document error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Search documents with faceted filtering - * GET /api/documents/search?q=...&quadrant=...&persistence=...&audience=... - */ -async function searchDocuments(req, res) { - try { - const { q, quadrant, persistence, audience, limit = 20, skip = 0 } = req.query; - - // Build filter for faceted search - const filter = { - visibility: 'public' - }; - - // Add facet filters - if (quadrant) { - filter.quadrant = quadrant.toUpperCase(); - } - if (persistence) { - filter.persistence = persistence.toUpperCase(); - } - if (audience) { - filter.audience = audience.toLowerCase(); - } - - let documents; - - // If text query provided, use full-text search with filters - if (q && q.trim()) { - const { getCollection } = require('../utils/db.util'); - const collection = await getCollection('documents'); - - // Add text search to filter - filter.$text = { $search: q }; - - documents = await collection - .find(filter, { score: { $meta: 'textScore' } }) - .sort({ score: { $meta: 'textScore' } }) - .skip(parseInt(skip)) - .limit(parseInt(limit)) - .toArray(); - } else { - // No text query - just filter by facets - documents = await Document.list({ - filter, - limit: parseInt(limit), - skip: parseInt(skip), - sort: { order: 1, 'metadata.date_created': -1 } - }); - } - - // Count total matching documents - const { getCollection } = require('../utils/db.util'); - const collection = await getCollection('documents'); - const total = await collection.countDocuments(filter); - - res.json({ - success: true, - query: q || null, - filters: { - quadrant: quadrant || null, - persistence: persistence || null, - audience: audience || null - }, - documents, - count: documents.length, - total, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip), - hasMore: parseInt(skip) + documents.length < total - } - }); - - } catch (error) { - logger.error('Search documents error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Create document (admin only) - * POST /api/documents - */ -async function createDocument(req, res) { - try { - const { title, slug, quadrant, persistence, audience, content_markdown, metadata } = req.body; - - // Convert markdown to HTML - const content_html = markdownToHtml(content_markdown); - - // Extract table of contents - const toc = extractTOC(content_markdown); - - // Create search index from content - const search_index = `${title} ${content_markdown}`.toLowerCase(); - - const document = await Document.create({ - title, - slug, - quadrant, - persistence, - audience: audience || 'general', - content_html, - content_markdown, - toc, - metadata, - search_index - }); - - logger.info(`Document created: ${slug} by ${req.user.email}`); - - res.status(201).json({ - success: true, - document - }); - - } catch (error) { - logger.error('Create document error:', error); - - // Handle duplicate slug - if (error.code === 11000) { - return res.status(409).json({ - error: 'Conflict', - message: 'A document with this slug already exists' - }); - } - - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Update document (admin only) - * PUT /api/documents/:id - */ -async function updateDocument(req, res) { - try { - const { id } = req.params; - const updates = { ...req.body }; - - // If content_markdown is updated, regenerate HTML and TOC - if (updates.content_markdown) { - updates.content_html = markdownToHtml(updates.content_markdown); - updates.toc = extractTOC(updates.content_markdown); - updates.search_index = `${updates.title || ''} ${updates.content_markdown}`.toLowerCase(); - } - - const success = await Document.update(id, updates); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Document not found' - }); - } - - const document = await Document.findById(id); - - logger.info(`Document updated: ${id} by ${req.user.email}`); - - res.json({ - success: true, - document - }); - - } catch (error) { - logger.error('Update document error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Delete document (admin only) - * DELETE /api/documents/:id - */ -async function deleteDocument(req, res) { - try { - const { id } = req.params; - - const success = await Document.delete(id); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Document not found' - }); - } - - logger.info(`Document deleted: ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Document deleted successfully' - }); - - } catch (error) { - logger.error('Delete document error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * List archived documents - * GET /api/documents/archived - */ -async function listArchivedDocuments(req, res) { - try { - const { limit = 50, skip = 0 } = req.query; - - const documents = await Document.listArchived({ - limit: parseInt(limit), - skip: parseInt(skip) - }); - - const total = await Document.count({ visibility: 'archived' }); - - res.json({ - success: true, - documents, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip), - hasMore: parseInt(skip) + documents.length < total - } - }); - - } catch (error) { - logger.error('List archived documents error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Publish a document (admin only) - * POST /api/documents/:id/publish - * - * SECURITY: Explicit publish workflow prevents accidental exposure - * World-class UX: Clear validation messages guide admins - */ -async function publishDocument(req, res) { - try { - const { id } = req.params; - const { category, order } = req.body; - - const result = await Document.publish(id, { - category, - order, - publishedBy: req.user?.email || 'admin' - }); - - if (!result.success) { - return res.status(400).json({ - error: 'Bad Request', - message: result.message - }); - } - - logger.info(`Document published: ${id} by ${req.user?.email || 'admin'} (category: ${category})`); - - res.json({ - success: true, - message: result.message, - document: result.document - }); - - } catch (error) { - logger.error('Publish document error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: error.message || 'An error occurred' - }); - } -} - -/** - * Unpublish a document (admin only) - * POST /api/documents/:id/unpublish - */ -async function unpublishDocument(req, res) { - try { - const { id } = req.params; - const { reason } = req.body; - - const result = await Document.unpublish(id, reason); - - if (!result.success) { - return res.status(404).json({ - error: 'Not Found', - message: result.message - }); - } - - logger.info(`Document unpublished: ${id} by ${req.user?.email || 'admin'} (reason: ${reason || 'none'})`); - - res.json({ - success: true, - message: result.message - }); - - } catch (error) { - logger.error('Unpublish document error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * List draft documents (admin only) - * GET /api/documents/drafts - */ -async function listDraftDocuments(req, res) { - try { - const { limit = 50, skip = 0 } = req.query; - - const documents = await Document.listByWorkflowStatus('draft', { - limit: parseInt(limit), - skip: parseInt(skip) - }); - - res.json({ - success: true, - documents, - pagination: { - total: documents.length, - limit: parseInt(limit), - skip: parseInt(skip) - } - }); - - } catch (error) { - logger.error('List draft documents error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -module.exports = { - listDocuments, - getDocument, - searchDocuments, - createDocument, - updateDocument, - deleteDocument, - listArchivedDocuments, - publishDocument, - unpublishDocument, - listDraftDocuments -}; diff --git a/src/controllers/koha.controller.js b/src/controllers/koha.controller.js deleted file mode 100644 index 0bb454d1..00000000 --- a/src/controllers/koha.controller.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Koha Controller - * Handles donation-related HTTP requests - */ - -const kohaService = require('../services/koha.service'); -const logger = require('../utils/logger.util'); - -/** - * Create checkout session for donation - * POST /api/koha/checkout - */ -exports.createCheckout = async (req, res) => { - try { - // Check if Stripe is configured (not placeholder) - if (!process.env.STRIPE_SECRET_KEY || - process.env.STRIPE_SECRET_KEY.includes('PLACEHOLDER')) { - return res.status(503).json({ - success: false, - error: 'Donation system not yet active', - message: 'The Koha donation system is currently being configured. Please check back soon.' - }); - } - - const { amount, frequency, tier, donor, public_acknowledgement, public_name } = req.body; - - // Validate required fields - if (!amount || !frequency || !donor?.email) { - return res.status(400).json({ - success: false, - error: 'Missing required fields: amount, frequency, donor.email' - }); - } - - // Validate amount - if (amount < 100) { - return res.status(400).json({ - success: false, - error: 'Minimum donation amount is NZD $1.00' - }); - } - - // Validate frequency - if (!['monthly', 'one_time'].includes(frequency)) { - return res.status(400).json({ - success: false, - error: 'Invalid frequency. Must be "monthly" or "one_time"' - }); - } - - // Validate tier for monthly donations - if (frequency === 'monthly' && !['5', '15', '50'].includes(tier)) { - return res.status(400).json({ - success: false, - error: 'Invalid tier for monthly donations. Must be "5", "15", or "50"' - }); - } - - // Create checkout session - const session = await kohaService.createCheckoutSession({ - amount, - frequency, - tier, - donor, - public_acknowledgement: public_acknowledgement || false, - public_name: public_name || null - }); - - logger.info(`[KOHA] Checkout session created: ${session.sessionId}`); - - res.status(200).json({ - success: true, - data: session - }); - - } catch (error) { - logger.error('[KOHA] Create checkout error:', error); - res.status(500).json({ - success: false, - error: error.message || 'Failed to create checkout session' - }); - } -}; - -/** - * Handle Stripe webhook events - * POST /api/koha/webhook - */ -exports.handleWebhook = async (req, res) => { - const signature = req.headers['stripe-signature']; - - try { - // Verify webhook signature and construct event - const event = kohaService.verifyWebhookSignature(req.rawBody, signature); - - // Process webhook event - await kohaService.handleWebhook(event); - - res.status(200).json({ received: true }); - - } catch (error) { - logger.error('[KOHA] Webhook error:', error); - res.status(400).json({ - success: false, - error: error.message || 'Webhook processing failed' - }); - } -}; - -/** - * Get public transparency metrics - * GET /api/koha/transparency - */ -exports.getTransparency = async (req, res) => { - try { - const metrics = await kohaService.getTransparencyMetrics(); - - res.status(200).json({ - success: true, - data: metrics - }); - - } catch (error) { - logger.error('[KOHA] Get transparency error:', error); - res.status(500).json({ - success: false, - error: 'Failed to fetch transparency metrics' - }); - } -}; - -/** - * Cancel recurring donation - * POST /api/koha/cancel - * Requires email verification to prevent unauthorized cancellations - */ -exports.cancelDonation = async (req, res) => { - try { - const { subscriptionId, email } = req.body; - - if (!subscriptionId || !email) { - return res.status(400).json({ - success: false, - error: 'Subscription ID and email are required' - }); - } - - // Verify donor owns this subscription by checking email - const donation = await require('../models/Donation.model').findBySubscriptionId(subscriptionId); - - if (!donation) { - return res.status(404).json({ - success: false, - error: 'Subscription not found' - }); - } - - // Verify email matches the donor's email - if (donation.donor.email.toLowerCase() !== email.toLowerCase()) { - logger.warn(`[KOHA SECURITY] Failed cancellation attempt: subscription ${subscriptionId} with wrong email ${email}`); - return res.status(403).json({ - success: false, - error: 'Email does not match subscription owner' - }); - } - - // Email verified, proceed with cancellation - const result = await kohaService.cancelRecurringDonation(subscriptionId); - - logger.info(`[KOHA] Subscription cancelled: ${subscriptionId} by ${email}`); - - res.status(200).json({ - success: true, - data: result - }); - - } catch (error) { - logger.error('[KOHA] Cancel donation error:', error); - res.status(500).json({ - success: false, - error: error.message || 'Failed to cancel donation' - }); - } -}; - -/** - * Get donation statistics (ADMIN ONLY) - * GET /api/koha/statistics - * Authentication enforced in routes layer (requireAdmin middleware) - */ -exports.getStatistics = async (req, res) => { - try { - const { startDate, endDate } = req.query; - - const statistics = await kohaService.getStatistics(startDate, endDate); - - res.status(200).json({ - success: true, - data: statistics - }); - - } catch (error) { - logger.error('[KOHA] Get statistics error:', error); - res.status(500).json({ - success: false, - error: 'Failed to fetch statistics' - }); - } -}; - -/** - * Verify donation session (after redirect from Stripe) - * GET /api/koha/verify/:sessionId - */ -exports.verifySession = async (req, res) => { - try { - const { sessionId } = req.params; - - if (!sessionId) { - return res.status(400).json({ - success: false, - error: 'Session ID is required' - }); - } - - // Retrieve session from Stripe - const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - const session = await stripe.checkout.sessions.retrieve(sessionId); - - // Check if payment was successful - const isSuccessful = session.payment_status === 'paid'; - - res.status(200).json({ - success: true, - data: { - status: session.payment_status, - amount: session.amount_total / 100, - currency: session.currency, - frequency: session.metadata.frequency, - isSuccessful: isSuccessful - } - }); - - } catch (error) { - logger.error('[KOHA] Verify session error:', error); - res.status(500).json({ - success: false, - error: 'Failed to verify session' - }); - } -}; - -/** - * Create Stripe Customer Portal session - * POST /api/koha/portal - * Allows donors to manage their subscription (update payment, cancel, etc.) - */ -exports.createPortalSession = async (req, res) => { - try { - const { email } = req.body; - - if (!email) { - return res.status(400).json({ - success: false, - error: 'Email is required' - }); - } - - // Find customer by email - const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - const customers = await stripe.customers.list({ - email: email, - limit: 1 - }); - - if (customers.data.length === 0) { - return res.status(404).json({ - success: false, - error: 'No subscription found for this email address' - }); - } - - const customer = customers.data[0]; - - // Create portal session - const session = await stripe.billingPortal.sessions.create({ - customer: customer.id, - return_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha.html`, - }); - - logger.info(`[KOHA] Customer portal session created for ${email}`); - - res.status(200).json({ - success: true, - data: { - url: session.url - } - }); - - } catch (error) { - logger.error('[KOHA] Create portal session error:', error); - res.status(500).json({ - success: false, - error: error.message || 'Failed to create portal session' - }); - } -}; diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js deleted file mode 100644 index f0854e49..00000000 --- a/src/controllers/media.controller.js +++ /dev/null @@ -1,447 +0,0 @@ -/** - * Media Inquiry Controller - * Press/media inquiry submission and AI triage - */ - -const MediaInquiry = require('../models/MediaInquiry.model'); -const ModerationQueue = require('../models/ModerationQueue.model'); -const GovernanceLog = require('../models/GovernanceLog.model'); -const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); -const MediaTriageService = require('../services/MediaTriage.service'); -const logger = require('../utils/logger.util'); - -/** - * Submit media inquiry (public) - * POST /api/media/inquiries - * - * Phase 1: Manual triage (no AI) - * Phase 2: Add AI triage with claudeAPI.triageMediaInquiry() - */ -async function submitInquiry(req, res) { - try { - const { contact, inquiry } = req.body; - - // Validate required fields - if (!contact?.name || !contact?.email || !contact?.outlet) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Missing required contact information' - }); - } - - if (!inquiry?.subject || !inquiry?.message) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Missing required inquiry information' - }); - } - - logger.info(`Media inquiry submitted: ${contact.outlet} - ${inquiry.subject}`); - - // Create inquiry (Phase 1: no AI triage yet) - const mediaInquiry = await MediaInquiry.create({ - contact, - inquiry, - status: 'new', - ai_triage: { - urgency: 'medium', // Default, will be AI-assessed in Phase 2 - topic_sensitivity: 'standard', - involves_values: false - } - }); - - // Add to moderation queue for human review - await ModerationQueue.create({ - type: 'MEDIA_INQUIRY', - reference_collection: 'media_inquiries', - reference_id: mediaInquiry._id, - quadrant: 'OPERATIONAL', - data: { - contact, - inquiry - }, - priority: 'medium', - status: 'PENDING_APPROVAL', - requires_human_approval: true, - human_required_reason: 'All media inquiries require human review and response' - }); - - logger.info(`Media inquiry created: ${mediaInquiry._id}`); - - res.status(201).json({ - success: true, - message: 'Thank you for your inquiry. We will review and respond shortly.', - inquiry_id: mediaInquiry._id, - governance: { - human_review: true, - note: 'All media inquiries are reviewed by humans before response' - } - }); - - } catch (error) { - logger.error('Submit inquiry error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred while submitting your inquiry' - }); - } -} - -/** - * List all media inquiries (admin) - * GET /api/media/inquiries?status=new - */ -async function listInquiries(req, res) { - try { - const { status = 'new', limit = 20, skip = 0 } = req.query; - - const inquiries = await MediaInquiry.findByStatus(status, { - limit: parseInt(limit), - skip: parseInt(skip) - }); - - const total = await MediaInquiry.countByStatus(status); - - res.json({ - success: true, - status, - inquiries, - pagination: { - total, - limit: parseInt(limit), - skip: parseInt(skip), - hasMore: parseInt(skip) + inquiries.length < total - } - }); - - } catch (error) { - logger.error('List inquiries error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * List urgent media inquiries (admin) - * GET /api/media/inquiries/urgent - */ -async function listUrgentInquiries(req, res) { - try { - const { limit = 10 } = req.query; - - const inquiries = await MediaInquiry.findUrgent({ - limit: parseInt(limit) - }); - - res.json({ - success: true, - count: inquiries.length, - inquiries - }); - - } catch (error) { - logger.error('List urgent inquiries error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Get media inquiry by ID (admin) - * GET /api/media/inquiries/:id - */ -async function getInquiry(req, res) { - try { - const { id } = req.params; - - const inquiry = await MediaInquiry.findById(id); - - if (!inquiry) { - return res.status(404).json({ - error: 'Not Found', - message: 'Media inquiry not found' - }); - } - - res.json({ - success: true, - inquiry - }); - - } catch (error) { - logger.error('Get inquiry error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Assign inquiry to user (admin) - * POST /api/media/inquiries/:id/assign - */ -async function assignInquiry(req, res) { - try { - const { id } = req.params; - const { user_id } = req.body; - - const userId = user_id || req.user._id; - - const success = await MediaInquiry.assign(id, userId); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Media inquiry not found' - }); - } - - logger.info(`Media inquiry ${id} assigned to ${userId} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Inquiry assigned successfully' - }); - - } catch (error) { - logger.error('Assign inquiry error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Respond to inquiry (admin) - * POST /api/media/inquiries/:id/respond - */ -async function respondToInquiry(req, res) { - try { - const { id } = req.params; - const { content } = req.body; - - if (!content) { - return res.status(400).json({ - error: 'Bad Request', - message: 'Response content is required' - }); - } - - const inquiry = await MediaInquiry.findById(id); - - if (!inquiry) { - return res.status(404).json({ - error: 'Not Found', - message: 'Media inquiry not found' - }); - } - - const success = await MediaInquiry.respond(id, { - content, - responder: req.user.email - }); - - if (!success) { - return res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to update inquiry' - }); - } - - logger.info(`Media inquiry ${id} responded to by ${req.user.email}`); - - res.json({ - success: true, - message: 'Response recorded successfully', - note: 'Remember to send actual email to media contact separately' - }); - - } catch (error) { - logger.error('Respond to inquiry error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Delete media inquiry (admin) - * DELETE /api/media/inquiries/:id - */ -async function deleteInquiry(req, res) { - try { - const { id } = req.params; - - const success = await MediaInquiry.delete(id); - - if (!success) { - return res.status(404).json({ - error: 'Not Found', - message: 'Media inquiry not found' - }); - } - - logger.info(`Media inquiry deleted: ${id} by ${req.user.email}`); - - res.json({ - success: true, - message: 'Inquiry deleted successfully' - }); - - } catch (error) { - logger.error('Delete inquiry error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'An error occurred' - }); - } -} - -/** - * Run AI triage on inquiry (admin) - * POST /api/media/inquiries/:id/triage - * - * Demonstrates Tractatus dogfooding: AI assists, human decides - */ -async function triageInquiry(req, res) { - try { - const { id } = req.params; - - const inquiry = await MediaInquiry.findById(id); - - if (!inquiry) { - return res.status(404).json({ - error: 'Not Found', - message: 'Media inquiry not found' - }); - } - - logger.info(`Running AI triage on inquiry ${id}`); - - // Run AI triage (MediaTriage service handles all analysis) - const triageResult = await MediaTriageService.triageInquiry(inquiry); - - // Update inquiry with triage results - await MediaInquiry.update(id, { - 'ai_triage.urgency': triageResult.urgency, - 'ai_triage.urgency_score': triageResult.urgency_score, - 'ai_triage.urgency_reasoning': triageResult.urgency_reasoning, - 'ai_triage.topic_sensitivity': triageResult.topic_sensitivity, - 'ai_triage.sensitivity_reasoning': triageResult.sensitivity_reasoning, - 'ai_triage.involves_values': triageResult.involves_values, - 'ai_triage.values_reasoning': triageResult.values_reasoning, - 'ai_triage.boundary_enforcement': triageResult.boundary_enforcement, - 'ai_triage.suggested_response_time': triageResult.suggested_response_time, - 'ai_triage.suggested_talking_points': triageResult.suggested_talking_points, - 'ai_triage.draft_response': triageResult.draft_response, - 'ai_triage.draft_response_reasoning': triageResult.draft_response_reasoning, - 'ai_triage.triaged_at': triageResult.triaged_at, - 'ai_triage.ai_model': triageResult.ai_model, - status: 'triaged' - }); - - // Log governance action - await GovernanceLog.create({ - action: 'AI_TRIAGE', - entity_type: 'media_inquiry', - entity_id: id, - actor: req.user.email, - quadrant: triageResult.involves_values ? 'STRATEGIC' : 'OPERATIONAL', - tractatus_component: 'BoundaryEnforcer', - reasoning: triageResult.values_reasoning, - outcome: 'success', - metadata: { - urgency: triageResult.urgency, - urgency_score: triageResult.urgency_score, - involves_values: triageResult.involves_values, - boundary_enforced: triageResult.involves_values, - human_approval_required: true - } - }); - - logger.info(`AI triage complete for inquiry ${id}: urgency=${triageResult.urgency}, values=${triageResult.involves_values}`); - - res.json({ - success: true, - message: 'AI triage completed', - triage: triageResult, - governance: { - human_approval_required: true, - boundary_enforcer_active: triageResult.involves_values, - transparency_note: 'All AI reasoning is visible for human review' - } - }); - - } catch (error) { - logger.error('Triage inquiry error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'AI triage failed', - details: error.message - }); - } -} - -/** - * Get triage statistics for public transparency - * GET /api/media/triage-stats - */ -async function getTriageStats(req, res) { - try { - // Get all triaged inquiries (public stats, no sensitive data) - const { getCollection } = require('../utils/db.util'); - const collection = await getCollection('media_inquiries'); - - const inquiries = await collection.find({ - 'ai_triage.triaged_at': { $exists: true } - }).toArray(); - - const stats = await MediaTriageService.getTriageStats(inquiries); - - // Add transparency metrics - const transparencyMetrics = { - ...stats, - human_review_rate: '100%', // All inquiries require human review - ai_auto_response_rate: '0%', // No auto-responses allowed - boundary_enforcement_active: stats.boundary_enforcements > 0, - framework_compliance: { - human_approval_required: true, - ai_reasoning_transparent: true, - values_decisions_escalated: true - } - }; - - res.json({ - success: true, - period: 'all_time', - statistics: transparencyMetrics, - note: 'All media inquiries require human review before response. AI assists with triage only.' - }); - - } catch (error) { - logger.error('Get triage stats error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to retrieve statistics' - }); - } -} - -module.exports = { - submitInquiry, - listInquiries, - listUrgentInquiries, - getInquiry, - assignInquiry, - respondToInquiry, - deleteInquiry, - triageInquiry, - getTriageStats -}; diff --git a/src/controllers/newsletter.controller.js b/src/controllers/newsletter.controller.js deleted file mode 100644 index dca5a7fe..00000000 --- a/src/controllers/newsletter.controller.js +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Newsletter Controller - * Handles newsletter subscriptions and management - */ - -const NewsletterSubscription = require('../models/NewsletterSubscription.model'); -const logger = require('../utils/logger.util'); - -/** - * Subscribe to newsletter (public) - * POST /api/newsletter/subscribe - */ -exports.subscribe = async (req, res) => { - try { - const { email, name, source, interests } = req.body; - - // Validate email - if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - return res.status(400).json({ - success: false, - error: 'Valid email address is required' - }); - } - - // Capture metadata - const metadata = { - ip_address: req.ip || req.connection.remoteAddress, - user_agent: req.get('user-agent'), - referrer: req.get('referer') - }; - - const subscription = await NewsletterSubscription.subscribe({ - email, - name, - source: source || 'blog', - interests: interests || [], - ...metadata - }); - - logger.info('Newsletter subscription created', { - email: subscription.email, - source: subscription.source, - verified: subscription.verified - }); - - // In a real system, send verification email here - // For now, we'll auto-verify (remove this in production) - await NewsletterSubscription.verify(subscription.verification_token); - - res.status(201).json({ - success: true, - message: 'Successfully subscribed to newsletter', - subscription: { - email: subscription.email, - verified: true - } - }); - } catch (subscribeError) { - console.error('Newsletter subscription error:', subscribeError); - res.status(500).json({ - success: false, - error: 'Failed to subscribe to newsletter', - details: process.env.NODE_ENV === 'development' ? subscribeError.message : undefined - }); - } -}; - -/** - * Verify email subscription - * GET /api/newsletter/verify/:token - */ -exports.verify = async (req, res) => { - try { - const { token } = req.params; - - const verified = await NewsletterSubscription.verify(token); - - if (!verified) { - return res.status(404).json({ - success: false, - error: 'Invalid or expired verification token' - }); - } - - res.json({ - success: true, - message: 'Email verified successfully' - }); - } catch (error) { - logger.error('Newsletter verification error:', error); - res.status(500).json({ - success: false, - error: 'Failed to verify email' - }); - } -}; - -/** - * Unsubscribe from newsletter (public) - * POST /api/newsletter/unsubscribe - */ -exports.unsubscribe = async (req, res) => { - try { - const { email, token } = req.body; - - if (!email && !token) { - return res.status(400).json({ - success: false, - error: 'Email or token is required' - }); - } - - const unsubscribed = await NewsletterSubscription.unsubscribe(email, token); - - if (!unsubscribed) { - return res.status(404).json({ - success: false, - error: 'Subscription not found' - }); - } - - logger.info('Newsletter unsubscribe', { email: email || 'via token' }); - - res.json({ - success: true, - message: 'Successfully unsubscribed from newsletter' - }); - } catch (error) { - logger.error('Newsletter unsubscribe error:', error); - res.status(500).json({ - success: false, - error: 'Failed to unsubscribe' - }); - } -}; - -/** - * Update subscription preferences (public) - * PUT /api/newsletter/preferences - */ -exports.updatePreferences = async (req, res) => { - try { - const { email, name, interests } = req.body; - - if (!email) { - return res.status(400).json({ - success: false, - error: 'Email is required' - }); - } - - const updated = await NewsletterSubscription.updatePreferences(email, { - name, - interests - }); - - if (!updated) { - return res.status(404).json({ - success: false, - error: 'Active subscription not found' - }); - } - - res.json({ - success: true, - message: 'Preferences updated successfully' - }); - } catch (error) { - logger.error('Newsletter preferences update error:', error); - res.status(500).json({ - success: false, - error: 'Failed to update preferences' - }); - } -}; - -/** - * Get newsletter statistics (admin only) - * GET /api/admin/newsletter/stats - */ -exports.getStats = async (req, res) => { - try { - const stats = await NewsletterSubscription.getStats(); - - res.json({ - success: true, - stats - }); - } catch (error) { - logger.error('Newsletter stats error:', error); - res.status(500).json({ - success: false, - error: 'Failed to retrieve statistics' - }); - } -}; - -/** - * List all subscriptions (admin only) - * GET /api/admin/newsletter/subscriptions - */ -exports.listSubscriptions = async (req, res) => { - try { - const { - limit = 100, - skip = 0, - active = 'true', - verified = null, - source = null - } = req.query; - - const options = { - limit: parseInt(limit), - skip: parseInt(skip), - active: active === 'true', - verified: verified === null ? null : verified === 'true', - source: source || null - }; - - const subscriptions = await NewsletterSubscription.list(options); - const total = await NewsletterSubscription.count({ - active: options.active, - ...(options.verified !== null && { verified: options.verified }), - ...(options.source && { source: options.source }) - }); - - res.json({ - success: true, - subscriptions, - pagination: { - total, - limit: options.limit, - skip: options.skip, - has_more: skip + subscriptions.length < total - } - }); - } catch (error) { - logger.error('Newsletter list error:', error); - res.status(500).json({ - success: false, - error: 'Failed to list subscriptions' - }); - } -}; - -/** - * Export subscriptions as CSV (admin only) - * GET /api/admin/newsletter/export - */ -exports.exportSubscriptions = async (req, res) => { - try { - const { active = 'true' } = req.query; - - const subscriptions = await NewsletterSubscription.list({ - active: active === 'true', - limit: 10000 - }); - - // Generate CSV - const csv = [ - 'Email,Name,Source,Verified,Subscribed At', - ...subscriptions.map(sub => - `${sub.email},"${sub.name || ''}",${sub.source},${sub.verified},${sub.subscribed_at.toISOString()}` - ) - ].join('\n'); - - res.setHeader('Content-Type', 'text/csv'); - res.setHeader('Content-Disposition', `attachment; filename="newsletter-subscriptions-${Date.now()}.csv"`); - res.send(csv); - } catch (error) { - logger.error('Newsletter export error:', error); - res.status(500).json({ - success: false, - error: 'Failed to export subscriptions' - }); - } -}; - -/** - * Delete subscription (admin only) - * DELETE /api/admin/newsletter/subscriptions/:id - */ -exports.deleteSubscription = async (req, res) => { - try { - const { id } = req.params; - - const deleted = await NewsletterSubscription.delete(id); - - if (!deleted) { - return res.status(404).json({ - success: false, - error: 'Subscription not found' - }); - } - - logger.info('Newsletter subscription deleted', { id }); - - res.json({ - success: true, - message: 'Subscription deleted successfully' - }); - } catch (error) { - logger.error('Newsletter delete error:', error); - res.status(500).json({ - success: false, - error: 'Failed to delete subscription' - }); - } -}; diff --git a/src/controllers/variables.controller.js b/src/controllers/variables.controller.js deleted file mode 100644 index 42a841ca..00000000 --- a/src/controllers/variables.controller.js +++ /dev/null @@ -1,436 +0,0 @@ -/** - * Variables Controller - * - * Handles CRUD operations for project-specific variable values. - * Variables enable context-aware rendering of governance rules. - * - * Endpoints: - * - GET /api/admin/projects/:projectId/variables - List variables for project - * - GET /api/admin/variables/global - Get all unique variable names - * - POST /api/admin/projects/:projectId/variables - Create/update variable - * - PUT /api/admin/projects/:projectId/variables/:name - Update variable value - * - DELETE /api/admin/projects/:projectId/variables/:name - Delete variable - */ - -const VariableValue = require('../models/VariableValue.model'); -const Project = require('../models/Project.model'); -const VariableSubstitutionService = require('../services/VariableSubstitution.service'); - -/** - * Get all variables for a project - * @route GET /api/admin/projects/:projectId/variables - * @param {string} projectId - Project identifier - * @query {string} category - Filter by category (optional) - */ -async function getProjectVariables(req, res) { - try { - const { projectId } = req.params; - const { category } = req.query; - - // Verify project exists - const project = await Project.findByProjectId(projectId); - if (!project) { - return res.status(404).json({ - success: false, - error: 'Project not found', - message: `No project found with ID: ${projectId}` - }); - } - - // Fetch variables - const variables = await VariableValue.findByProject(projectId, { category }); - - res.json({ - success: true, - projectId, - projectName: project.name, - variables, - total: variables.length - }); - - } catch (error) { - console.error('Error fetching project variables:', error); - res.status(500).json({ - success: false, - error: 'Failed to fetch variables', - message: error.message - }); - } -} - -/** - * Get all unique variable names across all rules - * @route GET /api/admin/variables/global - */ -async function getGlobalVariables(req, res) { - try { - // Get all unique variables from rules - const ruleVariables = await VariableSubstitutionService.getAllVariables(); - - // Get all unique variables currently defined - const definedVariables = await VariableValue.getAllVariableNames(); - - // Merge and add metadata - const variableMap = new Map(); - - // Add variables from rules - ruleVariables.forEach(v => { - variableMap.set(v.name, { - name: v.name, - usageCount: v.usageCount, - rules: v.rules, - isDefined: definedVariables.includes(v.name) - }); - }); - - // Add variables that are defined but not used in any rules - definedVariables.forEach(name => { - if (!variableMap.has(name)) { - variableMap.set(name, { - name, - usageCount: 0, - rules: [], - isDefined: true - }); - } - }); - - const allVariables = Array.from(variableMap.values()) - .sort((a, b) => b.usageCount - a.usageCount); - - res.json({ - success: true, - variables: allVariables, - total: allVariables.length, - statistics: { - totalVariables: allVariables.length, - usedInRules: ruleVariables.length, - definedButUnused: allVariables.filter(v => v.usageCount === 0).length - } - }); - - } catch (error) { - console.error('Error fetching global variables:', error); - res.status(500).json({ - success: false, - error: 'Failed to fetch global variables', - message: error.message - }); - } -} - -/** - * Create or update variable value for project (upsert) - * @route POST /api/admin/projects/:projectId/variables - * @param {string} projectId - Project identifier - * @body {string} variableName - Variable name (UPPER_SNAKE_CASE) - * @body {string} value - Variable value - * @body {string} description - Description (optional) - * @body {string} category - Category (optional) - * @body {string} dataType - Data type (optional) - */ -async function createOrUpdateVariable(req, res) { - try { - const { projectId } = req.params; - const { variableName, value, description, category, dataType, validationRules } = req.body; - - // Verify project exists - const project = await Project.findByProjectId(projectId); - if (!project) { - return res.status(404).json({ - success: false, - error: 'Project not found', - message: `No project found with ID: ${projectId}` - }); - } - - // Validate variable name format - if (!/^[A-Z][A-Z0-9_]*$/.test(variableName)) { - return res.status(400).json({ - success: false, - error: 'Invalid variable name', - message: 'Variable name must be UPPER_SNAKE_CASE (e.g., DB_NAME, API_KEY_2)' - }); - } - - // Upsert variable - const variable = await VariableValue.upsertValue(projectId, variableName, { - value, - description, - category, - dataType, - validationRules, - updatedBy: req.user?.email || 'system' - }); - - // Validate the value against rules - const validation = variable.validateValue(); - - res.json({ - success: true, - variable: variable.toObject(), - validation, - message: `Variable "${variableName}" ${variable.isNew ? 'created' : 'updated'} successfully for project "${project.name}"` - }); - - } catch (error) { - console.error('Error creating/updating variable:', error); - - // Handle validation errors - if (error.name === 'ValidationError') { - const errors = Object.values(error.errors).map(e => e.message); - return res.status(400).json({ - success: false, - error: 'Validation failed', - message: errors.join(', '), - details: error.errors - }); - } - - res.status(500).json({ - success: false, - error: 'Failed to create/update variable', - message: error.message - }); - } -} - -/** - * Update existing variable value - * @route PUT /api/admin/projects/:projectId/variables/:variableName - * @param {string} projectId - Project identifier - * @param {string} variableName - Variable name - * @body {Object} updates - Fields to update - */ -async function updateVariable(req, res) { - try { - const { projectId, variableName } = req.params; - const updates = req.body; - - // Find existing variable - const variable = await VariableValue.findValue(projectId, variableName); - - if (!variable) { - return res.status(404).json({ - success: false, - error: 'Variable not found', - message: `No variable "${variableName}" found for project "${projectId}"` - }); - } - - // Apply updates - const allowedFields = ['value', 'description', 'category', 'dataType', 'validationRules']; - allowedFields.forEach(field => { - if (updates[field] !== undefined) { - variable[field] = updates[field]; - } - }); - - variable.updatedBy = req.user?.email || 'system'; - await variable.save(); - - // Validate the new value - const validation = variable.validateValue(); - - res.json({ - success: true, - variable: variable.toObject(), - validation, - message: `Variable "${variableName}" updated successfully` - }); - - } catch (error) { - console.error('Error updating variable:', error); - - if (error.name === 'ValidationError') { - const errors = Object.values(error.errors).map(e => e.message); - return res.status(400).json({ - success: false, - error: 'Validation failed', - message: errors.join(', '), - details: error.errors - }); - } - - res.status(500).json({ - success: false, - error: 'Failed to update variable', - message: error.message - }); - } -} - -/** - * Delete variable - * @route DELETE /api/admin/projects/:projectId/variables/:variableName - * @param {string} projectId - Project identifier - * @param {string} variableName - Variable name - * @query {boolean} hard - If true, permanently delete; otherwise soft delete - */ -async function deleteVariable(req, res) { - try { - const { projectId, variableName } = req.params; - const { hard } = req.query; - - const variable = await VariableValue.findValue(projectId, variableName); - - if (!variable) { - return res.status(404).json({ - success: false, - error: 'Variable not found', - message: `No variable "${variableName}" found for project "${projectId}"` - }); - } - - if (hard === 'true') { - // Hard delete - permanently remove - await VariableValue.deleteOne({ projectId, variableName: variableName.toUpperCase() }); - - res.json({ - success: true, - message: `Variable "${variableName}" permanently deleted` - }); - } else { - // Soft delete - set active to false - await variable.deactivate(); - - res.json({ - success: true, - message: `Variable "${variableName}" deactivated. Use ?hard=true to permanently delete.` - }); - } - - } catch (error) { - console.error('Error deleting variable:', error); - res.status(500).json({ - success: false, - error: 'Failed to delete variable', - message: error.message - }); - } -} - -/** - * Validate project variables (check for missing required variables) - * @route GET /api/admin/projects/:projectId/variables/validate - * @param {string} projectId - Project identifier - */ -async function validateProjectVariables(req, res) { - try { - const { projectId } = req.params; - - // Verify project exists - const project = await Project.findByProjectId(projectId); - if (!project) { - return res.status(404).json({ - success: false, - error: 'Project not found', - message: `No project found with ID: ${projectId}` - }); - } - - // Validate variables - const validation = await VariableSubstitutionService.validateProjectVariables(projectId); - - res.json({ - success: true, - projectId, - projectName: project.name, - validation, - message: validation.complete - ? `All required variables are defined for project "${project.name}"` - : `Missing ${validation.missing.length} required variable(s) for project "${project.name}"` - }); - - } catch (error) { - console.error('Error validating project variables:', error); - res.status(500).json({ - success: false, - error: 'Failed to validate variables', - message: error.message - }); - } -} - -/** - * Batch create/update variables from array - * @route POST /api/admin/projects/:projectId/variables/batch - * @param {string} projectId - Project identifier - * @body {Array} variables - Array of variable objects - */ -async function batchUpsertVariables(req, res) { - try { - const { projectId } = req.params; - const { variables } = req.body; - - if (!Array.isArray(variables)) { - return res.status(400).json({ - success: false, - error: 'Invalid request', - message: 'variables must be an array' - }); - } - - // Verify project exists - const project = await Project.findByProjectId(projectId); - if (!project) { - return res.status(404).json({ - success: false, - error: 'Project not found', - message: `No project found with ID: ${projectId}` - }); - } - - const results = { - created: [], - updated: [], - failed: [] - }; - - // Process each variable - for (const varData of variables) { - try { - const variable = await VariableValue.upsertValue(projectId, varData.variableName, { - ...varData, - updatedBy: req.user?.email || 'system' - }); - - const action = variable.isNew ? 'created' : 'updated'; - results[action].push({ - variableName: varData.variableName, - value: varData.value - }); - - } catch (error) { - results.failed.push({ - variableName: varData.variableName, - error: error.message - }); - } - } - - res.json({ - success: true, - results, - message: `Batch operation complete: ${results.created.length} created, ${results.updated.length} updated, ${results.failed.length} failed` - }); - - } catch (error) { - console.error('Error batch upserting variables:', error); - res.status(500).json({ - success: false, - error: 'Failed to batch upsert variables', - message: error.message - }); - } -} - -module.exports = { - getProjectVariables, - getGlobalVariables, - createOrUpdateVariable, - updateVariable, - deleteVariable, - validateProjectVariables, - batchUpsertVariables -}; diff --git a/src/middleware/auth.middleware.js b/src/middleware/auth.middleware.js deleted file mode 100644 index a5e992b2..00000000 --- a/src/middleware/auth.middleware.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Authentication Middleware - * JWT-based authentication for admin routes - */ - -const { verifyToken, extractTokenFromHeader } = require('../utils/jwt.util'); -const { User } = require('../models'); -const logger = require('../utils/logger.util'); - -/** - * Verify JWT token and attach user to request - */ -async function authenticateToken(req, res, next) { - try { - const token = extractTokenFromHeader(req.headers.authorization); - - if (!token) { - return res.status(401).json({ - error: 'Authentication required', - message: 'No token provided' - }); - } - - // Verify token - const decoded = verifyToken(token); - - // Get user from database - const user = await User.findById(decoded.userId); - - if (!user) { - return res.status(401).json({ - error: 'Authentication failed', - message: 'User not found' - }); - } - - if (!user.active) { - return res.status(401).json({ - error: 'Authentication failed', - message: 'User account is inactive' - }); - } - - // Attach user to request - req.user = user; - req.userId = user._id; - - next(); - } catch (error) { - logger.error('Authentication error:', error); - - return res.status(401).json({ - error: 'Authentication failed', - message: error.message - }); - } -} - -/** - * Check if user has required role - */ -function requireRole(...roles) { - return (req, res, next) => { - if (!req.user) { - return res.status(401).json({ - error: 'Authentication required' - }); - } - - if (!roles.includes(req.user.role)) { - return res.status(403).json({ - error: 'Insufficient permissions', - message: `Required role: ${roles.join(' or ')}` - }); - } - - next(); - }; -} - -/** - * Optional authentication (attach user if token present, continue if not) - */ -async function optionalAuth(req, res, next) { - try { - const token = extractTokenFromHeader(req.headers.authorization); - - if (token) { - const decoded = verifyToken(token); - const user = await User.findById(decoded.userId); - - if (user && user.active) { - req.user = user; - req.userId = user._id; - } - } - } catch (error) { - // Silently fail - authentication is optional - logger.debug('Optional auth failed:', error.message); - } - - next(); -} - -/** - * Require admin role (convenience function) - */ -const requireAdmin = requireRole('admin'); - -module.exports = { - authenticateToken, - requireRole, - requireAdmin, - optionalAuth -}; diff --git a/src/middleware/csrf-protection.middleware.js b/src/middleware/csrf-protection.middleware.js deleted file mode 100644 index 4da885a1..00000000 --- a/src/middleware/csrf-protection.middleware.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * CSRF Protection Middleware (Modern Approach) - * - * Uses SameSite cookies + double-submit cookie pattern - * Replaces deprecated csurf package - * - * Reference: OWASP CSRF Prevention Cheat Sheet - * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html - */ - -const crypto = require('crypto'); -const { logSecurityEvent, getClientIp } = require('../utils/security-logger'); - -/** - * Generate CSRF token - */ -function generateCsrfToken() { - return crypto.randomBytes(32).toString('hex'); -} - -/** - * CSRF Protection Middleware - * - * Uses double-submit cookie pattern: - * 1. Server sets CSRF token in secure, SameSite cookie - * 2. Client must send same token in custom header (X-CSRF-Token) - * 3. Server validates cookie matches header - */ -function csrfProtection(req, res, next) { - // Skip GET, HEAD, OPTIONS (safe methods) - if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { - return next(); - } - - // Get CSRF token from cookie - const cookieToken = req.cookies['csrf-token']; - - // Get CSRF token from header - const headerToken = req.headers['x-csrf-token'] || req.headers['csrf-token']; - - // Validate tokens exist and match - if (!cookieToken || !headerToken || cookieToken !== headerToken) { - logSecurityEvent({ - type: 'csrf_violation', - sourceIp: getClientIp(req), - userId: req.user?.id, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - method: req.method, - hasCookie: !!cookieToken, - hasHeader: !!headerToken, - tokensMatch: cookieToken === headerToken - }, - action: 'blocked', - severity: 'high' - }); - - return res.status(403).json({ - error: 'Forbidden', - message: 'Invalid CSRF token', - code: 'CSRF_VALIDATION_FAILED' - }); - } - - next(); -} - -/** - * Middleware to set CSRF token cookie - * Apply this globally or on routes that need CSRF protection - */ -function setCsrfToken(req, res, next) { - // Only set cookie if it doesn't exist - if (!req.cookies['csrf-token']) { - const token = generateCsrfToken(); - - //Check if we're behind a proxy (X-Forwarded-Proto header) - const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https'; - - res.cookie('csrf-token', token, { - httpOnly: true, - secure: isSecure && process.env.NODE_ENV === 'production', - sameSite: 'strict', - maxAge: 24 * 60 * 60 * 1000 // 24 hours - }); - } - - next(); -} - -/** - * Endpoint to get CSRF token for client-side usage - * GET /api/csrf-token - * - * Returns the CSRF token from the cookie so client can include it in requests - */ -function getCsrfToken(req, res) { - const token = req.cookies['csrf-token']; - - if (!token) { - return res.status(400).json({ - error: 'Bad Request', - message: 'No CSRF token found. Visit the site first to receive a token.' - }); - } - - res.json({ - csrfToken: token - }); -} - -module.exports = { - csrfProtection, - setCsrfToken, - getCsrfToken, - generateCsrfToken -}; diff --git a/src/middleware/file-security.middleware.js b/src/middleware/file-security.middleware.js deleted file mode 100644 index 7ef1b134..00000000 --- a/src/middleware/file-security.middleware.js +++ /dev/null @@ -1,440 +0,0 @@ -/** - * File Security Middleware (inst_041 Implementation) - * - * Multi-layer file upload validation: - * 1. File type validation (magic number check) - * 2. ClamAV malware scanning - * 3. Size limits enforcement - * 4. Quarantine system for suspicious files - * 5. Comprehensive security logging - * - * Reference: docs/plans/security-implementation-roadmap.md Phase 2 - */ - -const path = require('path'); -const fs = require('fs').promises; -const { exec } = require('child_process'); -const { promisify } = require('util'); -const multer = require('multer'); -const { logSecurityEvent, getClientIp } = require('../utils/security-logger'); - -const execAsync = promisify(exec); - -// Configuration -const UPLOAD_DIR = process.env.UPLOAD_DIR || '/tmp/tractatus-uploads'; -const QUARANTINE_DIR = process.env.QUARANTINE_DIR || - (process.env.HOME ? `${process.env.HOME}/var/quarantine/tractatus` : '/var/quarantine/tractatus'); -const MAX_FILE_SIZE = { - document: 10 * 1024 * 1024, // 10MB - media: 50 * 1024 * 1024, // 50MB - default: 5 * 1024 * 1024 // 5MB -}; - -// Allowed MIME types (whitelist approach) -const ALLOWED_MIME_TYPES = { - document: [ - 'application/pdf', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'text/plain', - 'text/markdown' - ], - media: [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/webp', - 'video/mp4', - 'video/webm' - ] -}; - -/** - * Ensure upload and quarantine directories exist - */ -async function ensureDirectories() { - try { - await fs.mkdir(UPLOAD_DIR, { recursive: true, mode: 0o750 }); - await fs.mkdir(QUARANTINE_DIR, { recursive: true, mode: 0o750 }); - } catch (error) { - console.error('[FILE SECURITY] Failed to create directories:', error.message); - } -} - -// Initialize directories on module load -ensureDirectories(); - -/** - * Validate file type using magic number (file command) - * Prevents MIME type spoofing - */ -async function validateFileType(filePath, expectedMimeType) { - try { - const { stdout } = await execAsync(`file --mime-type -b "${filePath}"`); - const actualMimeType = stdout.trim(); - - if (actualMimeType !== expectedMimeType) { - return { - valid: false, - actualType: actualMimeType, - expectedType: expectedMimeType, - message: 'File type mismatch (possible MIME spoofing)' - }; - } - - return { valid: true, actualType: actualMimeType }; - } catch (error) { - return { - valid: false, - message: `File type validation failed: ${error.message}` - }; - } -} - -/** - * Scan file with ClamAV - * Tries clamdscan first (fast, requires daemon), falls back to clamscan (slower, no daemon) - */ -async function scanWithClamAV(filePath) { - try { - // Try clamdscan first (fast with daemon) - await execAsync(`clamdscan --no-summary "${filePath}"`); - return { clean: true, threat: null, scanner: 'clamdscan' }; - } catch (error) { - const output = error.stdout || error.stderr || ''; - - // Check if virus found - if (output.includes('FOUND')) { - const match = output.match(/(.+): (.+) FOUND/); - const threat = match ? match[2] : 'Unknown threat'; - return { clean: false, threat, scanner: 'clamdscan' }; - } - - // If daemon not available, fallback to clamscan - if (output.includes('Could not connect')) { - console.log('[FILE SECURITY] clamdscan daemon unavailable, using clamscan fallback'); - try { - const { stdout } = await execAsync(`clamscan --no-summary "${filePath}"`); - - if (stdout.includes('FOUND')) { - const match = stdout.match(/(.+): (.+) FOUND/); - const threat = match ? match[2] : 'Unknown threat'; - return { clean: false, threat, scanner: 'clamscan' }; - } - - return { clean: true, threat: null, scanner: 'clamscan' }; - } catch (clamscanError) { - const clamscanOutput = clamscanError.stdout || clamscanError.stderr || ''; - - if (clamscanOutput.includes('FOUND')) { - const match = clamscanOutput.match(/(.+): (.+) FOUND/); - const threat = match ? match[2] : 'Unknown threat'; - return { clean: false, threat, scanner: 'clamscan' }; - } - - return { - clean: false, - threat: null, - error: `ClamAV scan failed: ${clamscanError.message}` - }; - } - } - - // Other error - return { - clean: false, - threat: null, - error: `ClamAV scan failed: ${error.message}` - }; - } -} - -/** - * Move file to quarantine - * Handles cross-filesystem moves (copy + delete instead of rename) - */ -async function quarantineFile(filePath, reason, metadata) { - try { - const filename = path.basename(filePath); - const timestamp = new Date().toISOString().replace(/:/g, '-'); - const quarantinePath = path.join(QUARANTINE_DIR, `${timestamp}_${filename}`); - - // Ensure quarantine directory exists - await ensureDirectories(); - - // Use copyFile + unlink to handle cross-filesystem moves - // (fs.rename fails with EXDEV when source and dest are on different filesystems) - await fs.copyFile(filePath, quarantinePath); - await fs.unlink(filePath); - - // Create metadata file - const metadataPath = `${quarantinePath}.json`; - await fs.writeFile(metadataPath, JSON.stringify({ - original_path: filePath, - original_name: filename, - quarantine_reason: reason, - quarantine_time: new Date().toISOString(), - ...metadata - }, null, 2)); - - console.log(`[FILE SECURITY] File quarantined: ${filename} → ${quarantinePath}`); - return quarantinePath; - } catch (error) { - console.error('[FILE SECURITY] Quarantine failed:', error.message); - // Try to delete file if quarantine fails - try { - await fs.unlink(filePath); - } catch (unlinkError) { - console.error('[FILE SECURITY] Failed to delete file after quarantine failure:', unlinkError.message); - } - throw error; - } -} - -/** - * File security validation middleware - * Use after multer upload - */ -function createFileSecurityMiddleware(options = {}) { - const { - fileType = 'default', - allowedMimeTypes = ALLOWED_MIME_TYPES.document - } = options; - - return async (req, res, next) => { - // Skip if no file uploaded - if (!req.file && !req.files) { - return next(); - } - - const files = req.files || [req.file]; - const clientIp = getClientIp(req); - const userId = req.user?.id || 'anonymous'; - - try { - for (const file of files) { - if (!file) continue; - - const filePath = file.path; - const filename = file.originalname; - - // 1. Check MIME type is allowed - if (!allowedMimeTypes.includes(file.mimetype)) { - await fs.unlink(filePath); - - await logSecurityEvent({ - type: 'file_upload_rejected', - sourceIp: clientIp, - userId, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - filename, - mime_type: file.mimetype, - reason: 'MIME type not allowed' - }, - action: 'rejected', - severity: 'medium' - }); - - return res.status(400).json({ - error: 'Bad Request', - message: `File type ${file.mimetype} is not allowed`, - allowed_types: allowedMimeTypes - }); - } - - // 2. Validate file type with magic number - const typeValidation = await validateFileType(filePath, file.mimetype); - if (!typeValidation.valid) { - await quarantineFile(filePath, 'MIME_TYPE_MISMATCH', { - reported_mime: file.mimetype, - actual_mime: typeValidation.actualType, - user_id: userId, - source_ip: clientIp - }); - - await logSecurityEvent({ - type: 'file_upload_quarantined', - sourceIp: clientIp, - userId, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - filename, - reason: 'MIME type mismatch', - reported: file.mimetype, - actual: typeValidation.actualType - }, - action: 'quarantined', - severity: 'high' - }); - - return res.status(403).json({ - error: 'Forbidden', - message: 'File rejected due to security concerns', - code: 'FILE_TYPE_MISMATCH' - }); - } - - // 3. Scan with ClamAV - const scanResult = await scanWithClamAV(filePath); - - if (!scanResult.clean) { - if (scanResult.threat) { - // Malware detected - await quarantineFile(filePath, 'MALWARE_DETECTED', { - threat: scanResult.threat, - user_id: userId, - source_ip: clientIp - }); - - await logSecurityEvent({ - type: 'malware_detected', - sourceIp: clientIp, - userId, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - filename, - threat: scanResult.threat, - mime_type: file.mimetype - }, - action: 'quarantined', - severity: 'critical' - }); - - return res.status(403).json({ - error: 'Forbidden', - message: 'File rejected: Security threat detected', - code: 'MALWARE_DETECTED' - }); - } else { - // Scan failed - await logSecurityEvent({ - type: 'file_scan_failed', - sourceIp: clientIp, - userId, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - filename, - error: scanResult.error - }, - action: 'blocked', - severity: 'high' - }); - - return res.status(503).json({ - error: 'Service Unavailable', - message: 'File security scan unavailable. Please try again later.', - code: 'SCAN_FAILED' - }); - } - } - - // File passed all checks - await logSecurityEvent({ - type: 'file_upload_validated', - sourceIp: clientIp, - userId, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - filename, - mime_type: file.mimetype, - size: file.size - }, - action: 'allowed', - severity: 'low' - }); - } - - // All files passed validation - next(); - - } catch (error) { - console.error('[FILE SECURITY] Validation error:', error); - - await logSecurityEvent({ - type: 'file_validation_error', - sourceIp: clientIp, - userId, - endpoint: req.path, - userAgent: req.get('user-agent'), - details: { - error: error.message - }, - action: 'error', - severity: 'high' - }); - - return res.status(500).json({ - error: 'Internal Server Error', - message: 'File validation failed' - }); - } - }; -} - -/** - * Create multer storage configuration - */ -function createUploadStorage() { - return multer.diskStorage({ - destination: async (req, file, cb) => { - await ensureDirectories(); - cb(null, UPLOAD_DIR); - }, - filename: (req, file, cb) => { - const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`; - const ext = path.extname(file.originalname); - const basename = path.basename(file.originalname, ext); - const sanitized = basename.replace(/[^a-zA-Z0-9-_]/g, '_'); - cb(null, `${sanitized}-${uniqueSuffix}${ext}`); - } - }); -} - -/** - * Create multer upload middleware with security - */ -function createSecureUpload(options = {}) { - const { - fileType = 'default', - maxFileSize = MAX_FILE_SIZE.default, - allowedMimeTypes = ALLOWED_MIME_TYPES.document, - fieldName = 'file' - } = options; - - const upload = multer({ - storage: createUploadStorage(), - limits: { - fileSize: maxFileSize, - files: 1 // Single file upload by default - }, - fileFilter: (req, file, cb) => { - // Basic MIME type check (will be verified again with magic number) - if (allowedMimeTypes.includes(file.mimetype)) { - cb(null, true); - } else { - cb(new Error(`File type ${file.mimetype} not allowed`)); - } - } - }); - - return [ - upload.single(fieldName), - createFileSecurityMiddleware({ fileType, allowedMimeTypes }) - ]; -} - -module.exports = { - createFileSecurityMiddleware, - createSecureUpload, - validateFileType, - scanWithClamAV, - quarantineFile, - ALLOWED_MIME_TYPES, - MAX_FILE_SIZE -}; diff --git a/src/middleware/response-sanitization.middleware.js b/src/middleware/response-sanitization.middleware.js deleted file mode 100644 index 0b73985a..00000000 --- a/src/middleware/response-sanitization.middleware.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Response Sanitization Middleware (inst_013, inst_045) - * Prevents information disclosure in error responses - * - * QUICK WIN: Hide stack traces and sensitive data in production - * NEVER expose: stack traces, internal paths, environment details - */ - -/** - * Sanitize error responses - * Production: Generic error messages only - * Development: More detailed errors for debugging - */ -function sanitizeErrorResponse(err, req, res, next) { - const isProduction = process.env.NODE_ENV === 'production'; - - // Log full error details internally (always) - console.error('[ERROR]', { - message: err.message, - stack: err.stack, - path: req.path, - method: req.method, - ip: req.ip, - user: req.user?.id || req.user?.userId, - timestamp: new Date().toISOString() - }); - - // Determine status code - const statusCode = err.statusCode || err.status || 500; - - // Generic error messages for common status codes - const genericErrors = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 413: 'Payload Too Large', - 429: 'Too Many Requests', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable' - }; - - // Production: Generic error messages only - if (isProduction) { - return res.status(statusCode).json({ - error: genericErrors[statusCode] || 'Error', - message: err.message || 'An error occurred', - // NEVER include in production: stack, file paths, internal details - }); - } - - // Development: More detailed errors (but still sanitized) - res.status(statusCode).json({ - error: err.name || 'Error', - message: err.message, - statusCode, - // Stack trace only in development - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); -} - -/** - * Remove sensitive fields from objects - * Useful for sanitizing database results before sending to client - */ -function removeSensitiveFields(data, sensitiveFields = ['password', 'passwordHash', 'apiKey', 'secret', 'token']) { - if (Array.isArray(data)) { - return data.map(item => removeSensitiveFields(item, sensitiveFields)); - } - - if (typeof data === 'object' && data !== null) { - const sanitized = { ...data }; - - // Remove sensitive fields - for (const field of sensitiveFields) { - delete sanitized[field]; - } - - // Recursively sanitize nested objects - for (const key in sanitized) { - if (typeof sanitized[key] === 'object' && sanitized[key] !== null) { - sanitized[key] = removeSensitiveFields(sanitized[key], sensitiveFields); - } - } - - return sanitized; - } - - return data; -} - -/** - * Middleware to sanitize response data - * Apply before sending responses with user/database data - */ -function sanitizeResponseData(req, res, next) { - // Store original json method - const originalJson = res.json.bind(res); - - // Override json method to sanitize data - res.json = function(data) { - const sanitized = removeSensitiveFields(data); - return originalJson(sanitized); - }; - - next(); -} - -module.exports = { - sanitizeErrorResponse, - removeSensitiveFields, - sanitizeResponseData -}; diff --git a/src/models/BlogPost.model.js b/src/models/BlogPost.model.js deleted file mode 100644 index 5de71aee..00000000 --- a/src/models/BlogPost.model.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * BlogPost Model - * AI-curated blog with human oversight - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class BlogPost { - /** - * Create a new blog post - */ - static async create(data) { - const collection = await getCollection('blog_posts'); - - const post = { - title: data.title, - slug: data.slug, - author: { - type: data.author?.type || 'human', // 'human' or 'ai_curated' - name: data.author?.name || 'John Stroh', - claude_version: data.author?.claude_version - }, - content: data.content, - excerpt: data.excerpt, - featured_image: data.featured_image, - status: data.status || 'draft', // draft/pending/published/archived - moderation: { - ai_analysis: data.moderation?.ai_analysis, - human_reviewer: data.moderation?.human_reviewer, - review_notes: data.moderation?.review_notes, - approved_at: data.moderation?.approved_at - }, - tractatus_classification: { - quadrant: data.tractatus_classification?.quadrant || 'OPERATIONAL', - values_sensitive: data.tractatus_classification?.values_sensitive || false, - requires_strategic_review: data.tractatus_classification?.requires_strategic_review || false - }, - published_at: data.published_at, - tags: data.tags || [], - view_count: 0, - engagement: { - shares: 0, - comments: 0 - } - }; - - const result = await collection.insertOne(post); - return { ...post, _id: result.insertedId }; - } - - /** - * Find post by ID - */ - static async findById(id) { - const collection = await getCollection('blog_posts'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find post by slug - */ - static async findBySlug(slug) { - const collection = await getCollection('blog_posts'); - return await collection.findOne({ slug }); - } - - /** - * Find published posts - */ - static async findPublished(options = {}) { - const collection = await getCollection('blog_posts'); - const { limit = 10, skip = 0, sort = { published_at: -1 } } = options; - - return await collection - .find({ status: 'published' }) - .sort(sort) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find posts by status - */ - static async findByStatus(status, options = {}) { - const collection = await getCollection('blog_posts'); - const { limit = 20, skip = 0 } = options; - - return await collection - .find({ status }) - .sort({ _id: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Update post - */ - static async update(id, updates) { - const collection = await getCollection('blog_posts'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: updates } - ); - - return result.modifiedCount > 0; - } - - /** - * Publish post (change status + set published_at) - */ - static async publish(id, reviewerId) { - const collection = await getCollection('blog_posts'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - status: 'published', - published_at: new Date(), - 'moderation.human_reviewer': reviewerId, - 'moderation.approved_at': new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Increment view count - */ - static async incrementViews(id) { - const collection = await getCollection('blog_posts'); - - await collection.updateOne( - { _id: new ObjectId(id) }, - { $inc: { view_count: 1 } } - ); - } - - /** - * Delete post - */ - static async delete(id) { - const collection = await getCollection('blog_posts'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } - - /** - * Count posts by status - */ - static async countByStatus(status) { - const collection = await getCollection('blog_posts'); - return await collection.countDocuments({ status }); - } -} - -module.exports = BlogPost; diff --git a/src/models/CaseSubmission.model.js b/src/models/CaseSubmission.model.js deleted file mode 100644 index df9fea16..00000000 --- a/src/models/CaseSubmission.model.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * CaseSubmission Model - * Community case study submissions - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class CaseSubmission { - /** - * Create a new case submission - */ - static async create(data) { - const collection = await getCollection('case_submissions'); - - const submission = { - submitter: { - name: data.submitter.name, - email: data.submitter.email, - organization: data.submitter.organization, - public: data.submitter.public !== undefined ? data.submitter.public : false - }, - case_study: { - title: data.case_study.title, - description: data.case_study.description, - failure_mode: data.case_study.failure_mode, - tractatus_applicability: data.case_study.tractatus_applicability, - evidence: data.case_study.evidence || [], - attachments: data.case_study.attachments || [] - }, - ai_review: { - relevance_score: data.ai_review?.relevance_score, // 0-1 - completeness_score: data.ai_review?.completeness_score, // 0-1 - recommended_category: data.ai_review?.recommended_category, - suggested_improvements: data.ai_review?.suggested_improvements || [], - claude_analysis: data.ai_review?.claude_analysis - }, - moderation: { - status: data.moderation?.status || 'pending', // pending/approved/rejected/needs_info - reviewer: data.moderation?.reviewer, - review_notes: data.moderation?.review_notes, - reviewed_at: data.moderation?.reviewed_at - }, - published_case_id: data.published_case_id, - submitted_at: new Date() - }; - - const result = await collection.insertOne(submission); - return { ...submission, _id: result.insertedId }; - } - - /** - * Find submission by ID - */ - static async findById(id) { - const collection = await getCollection('case_submissions'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find by moderation status - */ - static async findByStatus(status, options = {}) { - const collection = await getCollection('case_submissions'); - const { limit = 20, skip = 0 } = options; - - return await collection - .find({ 'moderation.status': status }) - .sort({ submitted_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find high-relevance submissions pending review - */ - static async findHighRelevance(options = {}) { - const collection = await getCollection('case_submissions'); - const { limit = 10 } = options; - - return await collection - .find({ - 'moderation.status': 'pending', - 'ai_review.relevance_score': { $gte: 0.7 } - }) - .sort({ 'ai_review.relevance_score': -1 }) - .limit(limit) - .toArray(); - } - - /** - * Update submission - */ - static async update(id, updates) { - const collection = await getCollection('case_submissions'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: updates } - ); - - return result.modifiedCount > 0; - } - - /** - * Approve submission - */ - static async approve(id, reviewerId, notes = '') { - const collection = await getCollection('case_submissions'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - 'moderation.status': 'approved', - 'moderation.reviewer': reviewerId, - 'moderation.review_notes': notes, - 'moderation.reviewed_at': new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Reject submission - */ - static async reject(id, reviewerId, reason) { - const collection = await getCollection('case_submissions'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - 'moderation.status': 'rejected', - 'moderation.reviewer': reviewerId, - 'moderation.review_notes': reason, - 'moderation.reviewed_at': new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Request more information - */ - static async requestInfo(id, reviewerId, requestedInfo) { - const collection = await getCollection('case_submissions'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - 'moderation.status': 'needs_info', - 'moderation.reviewer': reviewerId, - 'moderation.review_notes': requestedInfo, - 'moderation.reviewed_at': new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Link to published case study - */ - static async linkPublished(id, publishedCaseId) { - const collection = await getCollection('case_submissions'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - published_case_id: new ObjectId(publishedCaseId), - 'moderation.status': 'approved' - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Count by status - */ - static async countByStatus(status) { - const collection = await getCollection('case_submissions'); - return await collection.countDocuments({ 'moderation.status': status }); - } - - /** - * Delete submission - */ - static async delete(id) { - const collection = await getCollection('case_submissions'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } -} - -module.exports = CaseSubmission; diff --git a/src/models/Document.model.js b/src/models/Document.model.js deleted file mode 100644 index e1a39fcf..00000000 --- a/src/models/Document.model.js +++ /dev/null @@ -1,350 +0,0 @@ -/** - * Document Model - * Technical papers, framework documentation, specifications - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class Document { - /** - * Create a new document - * - * SECURITY: All new documents default to 'internal' visibility to prevent accidental exposure. - * Use publish() method to make documents public after review. - */ - static async create(data) { - const collection = await getCollection('documents'); - - // SECURITY: Require explicit visibility or default to 'internal' for safety - const visibility = data.visibility || 'internal'; - - // SECURITY: Validate visibility is a known value - const validVisibility = ['public', 'internal', 'confidential', 'archived']; - if (!validVisibility.includes(visibility)) { - throw new Error(`Invalid visibility: ${visibility}. Must be one of: ${validVisibility.join(', ')}`); - } - - // SECURITY: Prevent accidental public uploads - require category for public docs - if (visibility === 'public' && (!data.category || data.category === 'none')) { - throw new Error('Public documents must have a valid category (not "none")'); - } - - const document = { - title: data.title, - slug: data.slug, - quadrant: data.quadrant, // STR/OPS/TAC/SYS/STO - persistence: data.persistence, // HIGH/MEDIUM/LOW/VARIABLE - audience: data.audience || 'general', // technical, general, researcher, implementer, advocate, business, developer - visibility, // SECURITY: Defaults to 'internal', explicit required for 'public' - category: data.category || 'none', // conceptual, practical, reference, archived, project-tracking, research-proposal, research-topic - order: data.order || 999, // Display order (1-999, lower = higher priority) - archiveNote: data.archiveNote || null, // Explanation for why document was archived - workflow_status: 'draft', // SECURITY: Track publish workflow (draft, review, published) - content_html: data.content_html, - content_markdown: data.content_markdown, - toc: data.toc || [], - security_classification: data.security_classification || { - contains_credentials: false, - contains_financial_info: false, - contains_vulnerability_info: false, - contains_infrastructure_details: false, - requires_authentication: false - }, - metadata: { - author: data.metadata?.author || 'John Stroh', - date_created: new Date(), - date_updated: new Date(), - version: data.metadata?.version || '1.0', - document_code: data.metadata?.document_code, - related_documents: data.metadata?.related_documents || [], - tags: data.metadata?.tags || [] - }, - translations: data.translations || {}, - search_index: data.search_index || '', - download_formats: data.download_formats || {} - }; - - const result = await collection.insertOne(document); - return { ...document, _id: result.insertedId }; - } - - /** - * Find document by ID - */ - static async findById(id) { - const collection = await getCollection('documents'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find document by slug - */ - static async findBySlug(slug) { - const collection = await getCollection('documents'); - return await collection.findOne({ slug }); - } - - /** - * Find documents by quadrant - */ - static async findByQuadrant(quadrant, options = {}) { - const collection = await getCollection('documents'); - const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 }, publicOnly = false } = options; - - const filter = { quadrant }; - if (publicOnly) { - filter.visibility = 'public'; - } - - return await collection - .find(filter) - .sort(sort) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find documents by audience - */ - static async findByAudience(audience, options = {}) { - const collection = await getCollection('documents'); - const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 }, publicOnly = false } = options; - - const filter = { audience }; - if (publicOnly) { - filter.visibility = 'public'; - } - - return await collection - .find(filter) - .sort(sort) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Search documents - */ - static async search(query, options = {}) { - const collection = await getCollection('documents'); - const { limit = 20, skip = 0, publicOnly = false } = options; - - const filter = { $text: { $search: query } }; - if (publicOnly) { - filter.visibility = 'public'; - } - - return await collection - .find( - filter, - { score: { $meta: 'textScore' } } - ) - .sort({ score: { $meta: 'textScore' } }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Update document - */ - static async update(id, updates) { - const collection = await getCollection('documents'); - - // If updates contains metadata, merge date_updated into it - // Otherwise set it as a separate field - const updatePayload = { ...updates }; - if (updatePayload.metadata) { - updatePayload.metadata = { - ...updatePayload.metadata, - date_updated: new Date() - }; - } - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - ...updatePayload, - ...(updatePayload.metadata ? {} : { 'metadata.date_updated': new Date() }) - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Delete document - */ - static async delete(id) { - const collection = await getCollection('documents'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } - - /** - * Publish a document (make it public after review) - * - * SECURITY: Requires explicit category and validates document is ready - * World-class UX: Clear workflow states and validation feedback - * - * @param {string} id - Document ID - * @param {object} options - Publish options - * @param {string} options.category - Required category for public docs - * @param {number} options.order - Display order - * @returns {Promise<{success: boolean, message: string, document?: object}>} - */ - static async publish(id, options = {}) { - const collection = await getCollection('documents'); - - // Get document - const doc = await this.findById(id); - if (!doc) { - return { success: false, message: 'Document not found' }; - } - - // Validate document is ready for publishing - if (!doc.content_markdown && !doc.content_html) { - return { success: false, message: 'Document must have content before publishing' }; - } - - // SECURITY: Require valid category for public docs - const category = options.category || doc.category; - if (!category || category === 'none') { - return { - success: false, - message: 'Document must have a valid category before publishing. Available categories: getting-started, technical-reference, research-theory, advanced-topics, case-studies, business-leadership, archives' - }; - } - - // Validate category is in allowed list - const validCategories = [ - 'getting-started', 'technical-reference', 'research-theory', - 'advanced-topics', 'case-studies', 'business-leadership', 'archives' - ]; - if (!validCategories.includes(category)) { - return { - success: false, - message: `Invalid category: ${category}. Must be one of: ${validCategories.join(', ')}` - }; - } - - // Update to public - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - visibility: 'public', - category, - order: options.order !== undefined ? options.order : doc.order, - workflow_status: 'published', - 'metadata.date_updated': new Date(), - 'metadata.published_date': new Date(), - 'metadata.published_by': options.publishedBy || 'admin' - } - } - ); - - if (result.modifiedCount > 0) { - const updatedDoc = await this.findById(id); - return { - success: true, - message: `Document published successfully in category: ${category}`, - document: updatedDoc - }; - } - - return { success: false, message: 'Failed to publish document' }; - } - - /** - * Unpublish a document (revert to internal) - * - * @param {string} id - Document ID - * @param {string} reason - Reason for unpublishing - * @returns {Promise<{success: boolean, message: string}>} - */ - static async unpublish(id, reason = '') { - const collection = await getCollection('documents'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - visibility: 'internal', - workflow_status: 'draft', - 'metadata.date_updated': new Date(), - 'metadata.unpublished_date': new Date(), - 'metadata.unpublish_reason': reason - } - } - ); - - return result.modifiedCount > 0 - ? { success: true, message: 'Document unpublished successfully' } - : { success: false, message: 'Failed to unpublish document' }; - } - - /** - * List documents by workflow status - * - * @param {string} status - Workflow status (draft, review, published) - * @param {object} options - List options - * @returns {Promise} - */ - static async listByWorkflowStatus(status, options = {}) { - const collection = await getCollection('documents'); - const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 } } = options; - - return await collection - .find({ workflow_status: status }) - .sort(sort) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * List all documents - */ - static async list(options = {}) { - const collection = await getCollection('documents'); - const { limit = 50, skip = 0, sort = { order: 1, 'metadata.date_created': -1 }, filter = {} } = options; - - return await collection - .find(filter) - .sort(sort) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * List archived documents - */ - static async listArchived(options = {}) { - const collection = await getCollection('documents'); - const { limit = 50, skip = 0, sort = { 'metadata.date_created': -1 } } = options; - - return await collection - .find({ visibility: 'archived' }) - .sort(sort) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Count documents - */ - static async count(filter = {}) { - const collection = await getCollection('documents'); - return await collection.countDocuments(filter); - } -} - -module.exports = Document; diff --git a/src/models/Donation.model.js b/src/models/Donation.model.js deleted file mode 100644 index a2f955e6..00000000 --- a/src/models/Donation.model.js +++ /dev/null @@ -1,333 +0,0 @@ -/** - * Donation Model - * Koha (donation) system for Tractatus Framework support - * - * Privacy-first design: - * - Anonymous donations by default - * - Opt-in public acknowledgement - * - Email stored securely for receipts only - * - Public transparency metrics - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class Donation { - /** - * Create a new donation record - */ - static async create(data) { - const collection = await getCollection('koha_donations'); - - const donation = { - // Donation details - amount: data.amount, // In cents (in the specified currency) - currency: data.currency || 'nzd', - amount_nzd: data.amount_nzd || data.amount, // Amount in NZD for transparency calculations - exchange_rate_to_nzd: data.exchange_rate_to_nzd || 1.0, // Exchange rate at donation time - frequency: data.frequency, // 'monthly' or 'one_time' - tier: data.tier, // '5', '15', '50', or 'custom' - - // Donor information (private) - donor: { - name: data.donor?.name || 'Anonymous', - email: data.donor?.email, // Required for receipt, kept private - country: data.donor?.country, - // Do NOT store full address unless required for tax purposes - }, - - // Public acknowledgement (opt-in) - public_acknowledgement: data.public_acknowledgement || false, - public_name: data.public_name || null, // Name to show publicly if opted in - - // Stripe integration - stripe: { - customer_id: data.stripe?.customer_id, - subscription_id: data.stripe?.subscription_id, // For monthly donations - payment_intent_id: data.stripe?.payment_intent_id, - charge_id: data.stripe?.charge_id, - invoice_id: data.stripe?.invoice_id - }, - - // Status tracking - status: data.status || 'pending', // pending, completed, failed, cancelled, refunded - payment_date: data.payment_date || new Date(), - - // Receipt tracking - receipt: { - sent: false, - sent_date: null, - receipt_number: null - }, - - // Metadata - metadata: { - source: data.metadata?.source || 'website', // website, api, manual - campaign: data.metadata?.campaign, - referrer: data.metadata?.referrer, - user_agent: data.metadata?.user_agent, - ip_country: data.metadata?.ip_country - }, - - // Timestamps - created_at: new Date(), - updated_at: new Date() - }; - - const result = await collection.insertOne(donation); - return { ...donation, _id: result.insertedId }; - } - - /** - * Find donation by ID - */ - static async findById(id) { - const collection = await getCollection('koha_donations'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find donation by Stripe subscription ID - */ - static async findBySubscriptionId(subscriptionId) { - const collection = await getCollection('koha_donations'); - return await collection.findOne({ 'stripe.subscription_id': subscriptionId }); - } - - /** - * Find donation by Stripe payment intent ID - */ - static async findByPaymentIntentId(paymentIntentId) { - const collection = await getCollection('koha_donations'); - return await collection.findOne({ 'stripe.payment_intent_id': paymentIntentId }); - } - - /** - * Find all donations by donor email (for admin/receipt purposes) - */ - static async findByDonorEmail(email) { - const collection = await getCollection('koha_donations'); - return await collection.find({ 'donor.email': email }).sort({ created_at: -1 }).toArray(); - } - - /** - * Update donation status - */ - static async updateStatus(id, status, additionalData = {}) { - const collection = await getCollection('koha_donations'); - - const updateData = { - status, - updated_at: new Date(), - ...additionalData - }; - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: updateData } - ); - - return result.modifiedCount > 0; - } - - /** - * Mark receipt as sent - */ - static async markReceiptSent(id, receiptNumber) { - const collection = await getCollection('koha_donations'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - 'receipt.sent': true, - 'receipt.sent_date': new Date(), - 'receipt.receipt_number': receiptNumber, - updated_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Cancel recurring donation (subscription) - */ - static async cancelSubscription(subscriptionId) { - const collection = await getCollection('koha_donations'); - - const result = await collection.updateOne( - { 'stripe.subscription_id': subscriptionId }, - { - $set: { - status: 'cancelled', - updated_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Get transparency metrics (PUBLIC DATA) - * Returns aggregated data for public transparency dashboard - */ - static async getTransparencyMetrics() { - const collection = await getCollection('koha_donations'); - - // Get all completed donations - const completedDonations = await collection.find({ - status: 'completed' - }).toArray(); - - // Calculate totals (convert all to NZD for consistent totals) - const totalReceived = completedDonations.reduce((sum, d) => { - // Use amount_nzd if available, otherwise use amount (backwards compatibility) - const nzdAmount = d.amount_nzd || d.amount; - return sum + nzdAmount; - }, 0) / 100; // Convert cents to dollars - - // Count monthly supporters (active subscriptions) - const monthlyDonations = completedDonations.filter(d => d.frequency === 'monthly'); - const activeSubscriptions = monthlyDonations.filter(d => d.status === 'completed'); - const monthlySupporters = new Set(activeSubscriptions.map(d => d.stripe.customer_id)).size; - - // Count one-time donations - const oneTimeDonations = completedDonations.filter(d => d.frequency === 'one_time').length; - - // Get public acknowledgements (donors who opted in) - const publicDonors = completedDonations - .filter(d => d.public_acknowledgement && d.public_name) - .sort((a, b) => b.created_at - a.created_at) - .slice(0, 20) // Latest 20 donors - .map(d => ({ - name: d.public_name, - amount: d.amount / 100, - currency: d.currency || 'nzd', - amount_nzd: (d.amount_nzd || d.amount) / 100, - date: d.created_at, - frequency: d.frequency - })); - - // Calculate monthly recurring revenue (in NZD) - const monthlyRevenue = activeSubscriptions.reduce((sum, d) => { - const nzdAmount = d.amount_nzd || d.amount; - return sum + nzdAmount; - }, 0) / 100; - - // Allocation breakdown (as per specification) - const allocation = { - hosting: 0.30, - development: 0.40, - research: 0.20, - community: 0.10 - }; - - return { - total_received: totalReceived, - monthly_supporters: monthlySupporters, - one_time_donations: oneTimeDonations, - monthly_recurring_revenue: monthlyRevenue, - allocation: allocation, - recent_donors: publicDonors, - last_updated: new Date() - }; - } - - /** - * Get donation statistics (ADMIN ONLY) - */ - static async getStatistics(startDate = null, endDate = null) { - const collection = await getCollection('koha_donations'); - - const query = { status: 'completed' }; - if (startDate || endDate) { - query.created_at = {}; - if (startDate) query.created_at.$gte = new Date(startDate); - if (endDate) query.created_at.$lte = new Date(endDate); - } - - const donations = await collection.find(query).toArray(); - - return { - total_count: donations.length, - total_amount: donations.reduce((sum, d) => sum + d.amount, 0) / 100, - by_frequency: { - monthly: donations.filter(d => d.frequency === 'monthly').length, - one_time: donations.filter(d => d.frequency === 'one_time').length - }, - by_tier: { - tier_5: donations.filter(d => d.tier === '5').length, - tier_15: donations.filter(d => d.tier === '15').length, - tier_50: donations.filter(d => d.tier === '50').length, - custom: donations.filter(d => d.tier === 'custom').length - }, - average_donation: donations.length > 0 - ? (donations.reduce((sum, d) => sum + d.amount, 0) / donations.length) / 100 - : 0, - public_acknowledgements: donations.filter(d => d.public_acknowledgement).length - }; - } - - /** - * Get all donations (ADMIN ONLY - paginated) - */ - static async findAll(options = {}) { - const collection = await getCollection('koha_donations'); - - const { - page = 1, - limit = 20, - status = null, - frequency = null, - sortBy = 'created_at', - sortOrder = -1 - } = options; - - const query = {}; - if (status) query.status = status; - if (frequency) query.frequency = frequency; - - const skip = (page - 1) * limit; - - const donations = await collection - .find(query) - .sort({ [sortBy]: sortOrder }) - .skip(skip) - .limit(limit) - .toArray(); - - const total = await collection.countDocuments(query); - - return { - donations, - pagination: { - page, - limit, - total, - pages: Math.ceil(total / limit) - } - }; - } - - /** - * Create database indexes for performance - */ - static async createIndexes() { - const collection = await getCollection('koha_donations'); - - await collection.createIndex({ status: 1 }); - await collection.createIndex({ frequency: 1 }); - await collection.createIndex({ 'stripe.subscription_id': 1 }); - await collection.createIndex({ 'stripe.payment_intent_id': 1 }); - await collection.createIndex({ 'donor.email': 1 }); - await collection.createIndex({ created_at: -1 }); - await collection.createIndex({ public_acknowledgement: 1 }); - - return true; - } -} - -module.exports = Donation; diff --git a/src/models/MediaInquiry.model.js b/src/models/MediaInquiry.model.js deleted file mode 100644 index 4314ec45..00000000 --- a/src/models/MediaInquiry.model.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * MediaInquiry Model - * Press/media inquiries with AI triage - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class MediaInquiry { - /** - * Create a new media inquiry - */ - static async create(data) { - const collection = await getCollection('media_inquiries'); - - const inquiry = { - contact: { - name: data.contact.name, - email: data.contact.email, - outlet: data.contact.outlet, - phone: data.contact.phone - }, - inquiry: { - subject: data.inquiry.subject, - message: data.inquiry.message, - deadline: data.inquiry.deadline ? new Date(data.inquiry.deadline) : null, - topic_areas: data.inquiry.topic_areas || [] - }, - ai_triage: { - urgency: data.ai_triage?.urgency, // high/medium/low - topic_sensitivity: data.ai_triage?.topic_sensitivity, - suggested_response_time: data.ai_triage?.suggested_response_time, - involves_values: data.ai_triage?.involves_values || false, - claude_summary: data.ai_triage?.claude_summary, - suggested_talking_points: data.ai_triage?.suggested_talking_points || [] - }, - status: data.status || 'new', // new/triaged/responded/closed - assigned_to: data.assigned_to, - response: { - sent_at: data.response?.sent_at, - content: data.response?.content, - responder: data.response?.responder - }, - created_at: new Date() - }; - - const result = await collection.insertOne(inquiry); - return { ...inquiry, _id: result.insertedId }; - } - - /** - * Find inquiry by ID - */ - static async findById(id) { - const collection = await getCollection('media_inquiries'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find inquiries by status - */ - static async findByStatus(status, options = {}) { - const collection = await getCollection('media_inquiries'); - const { limit = 20, skip = 0 } = options; - - return await collection - .find({ status }) - .sort({ created_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find high urgency inquiries - */ - static async findUrgent(options = {}) { - const collection = await getCollection('media_inquiries'); - const { limit = 10 } = options; - - return await collection - .find({ - 'ai_triage.urgency': 'high', - status: { $in: ['new', 'triaged'] } - }) - .sort({ created_at: -1 }) - .limit(limit) - .toArray(); - } - - /** - * Update inquiry - */ - static async update(id, updates) { - const collection = await getCollection('media_inquiries'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: updates } - ); - - return result.modifiedCount > 0; - } - - /** - * Assign inquiry to user - */ - static async assign(id, userId) { - const collection = await getCollection('media_inquiries'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - assigned_to: new ObjectId(userId), - status: 'triaged' - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Mark as responded - */ - static async respond(id, responseData) { - const collection = await getCollection('media_inquiries'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - status: 'responded', - 'response.sent_at': new Date(), - 'response.content': responseData.content, - 'response.responder': responseData.responder - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Count by status - */ - static async countByStatus(status) { - const collection = await getCollection('media_inquiries'); - return await collection.countDocuments({ status }); - } - - /** - * Delete inquiry - */ - static async delete(id) { - const collection = await getCollection('media_inquiries'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } -} - -module.exports = MediaInquiry; diff --git a/src/models/ModerationQueue.model.js b/src/models/ModerationQueue.model.js deleted file mode 100644 index 7861b031..00000000 --- a/src/models/ModerationQueue.model.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * ModerationQueue Model - * Human oversight queue for AI actions - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class ModerationQueue { - /** - * Add item to moderation queue - */ - static async create(data) { - const collection = await getCollection('moderation_queue'); - - const item = { - // Type of moderation (NEW flexible field) - type: data.type, // BLOG_TOPIC_SUGGESTION/MEDIA_INQUIRY/CASE_STUDY/etc. - - // Reference to specific item (optional - not needed for suggestions) - reference_collection: data.reference_collection || null, // blog_posts/media_inquiries/etc. - reference_id: data.reference_id ? new ObjectId(data.reference_id) : null, - - // Tractatus quadrant - quadrant: data.quadrant || null, // STR/OPS/TAC/SYS/STO - - // AI action data (flexible object) - data: data.data || {}, // Flexible data field for AI outputs - - // AI metadata - ai_generated: data.ai_generated || false, - ai_version: data.ai_version || 'claude-sonnet-4-5', - - // Human oversight - requires_human_approval: data.requires_human_approval || true, - human_required_reason: data.human_required_reason || 'AI-generated content requires human review', - - // Priority and assignment - priority: data.priority || 'medium', // high/medium/low - assigned_to: data.assigned_to || null, - - // Status tracking - status: data.status || 'PENDING_APPROVAL', // PENDING_APPROVAL/APPROVED/REJECTED - created_at: data.created_at || new Date(), - created_by: data.created_by ? new ObjectId(data.created_by) : null, - reviewed_at: null, - - // Review decision - review_decision: { - action: null, // approve/reject/modify/escalate - notes: null, - reviewer: null - }, - - // Metadata - metadata: data.metadata || {}, - - // Legacy fields for backwards compatibility - item_type: data.item_type || null, - item_id: data.item_id ? new ObjectId(data.item_id) : null - }; - - const result = await collection.insertOne(item); - return { ...item, _id: result.insertedId }; - } - - /** - * Find item by ID - */ - static async findById(id) { - const collection = await getCollection('moderation_queue'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find pending items - */ - static async findPending(options = {}) { - const collection = await getCollection('moderation_queue'); - const { limit = 20, skip = 0, priority } = options; - - const filter = { status: 'pending' }; - if (priority) filter.priority = priority; - - return await collection - .find(filter) - .sort({ - priority: -1, // high first - created_at: 1 // oldest first - }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find by item type - */ - static async findByType(itemType, options = {}) { - const collection = await getCollection('moderation_queue'); - const { limit = 20, skip = 0 } = options; - - return await collection - .find({ item_type: itemType, status: 'pending' }) - .sort({ created_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find by quadrant - */ - static async findByQuadrant(quadrant, options = {}) { - const collection = await getCollection('moderation_queue'); - const { limit = 20, skip = 0 } = options; - - return await collection - .find({ quadrant, status: 'pending' }) - .sort({ created_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Review item (approve/reject/modify/escalate) - */ - static async review(id, decision) { - const collection = await getCollection('moderation_queue'); - - // Map action to status - const statusMap = { - 'approve': 'approved', - 'reject': 'rejected', - 'escalate': 'escalated' - }; - const status = statusMap[decision.action] || 'reviewed'; - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - status: status, - reviewed_at: new Date(), - 'review_decision.action': decision.action, - 'review_decision.notes': decision.notes, - 'review_decision.reviewer': decision.reviewer - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Approve item - */ - static async approve(id, reviewerId, notes = '') { - return await this.review(id, { - action: 'approve', - notes, - reviewer: reviewerId - }); - } - - /** - * Reject item - */ - static async reject(id, reviewerId, notes) { - return await this.review(id, { - action: 'reject', - notes, - reviewer: reviewerId - }); - } - - /** - * Escalate to strategic review - */ - static async escalate(id, reviewerId, reason) { - const collection = await getCollection('moderation_queue'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - quadrant: 'STRATEGIC', - priority: 'high', - human_required_reason: `ESCALATED: ${reason}`, - 'review_decision.action': 'escalate', - 'review_decision.notes': reason, - 'review_decision.reviewer': reviewerId - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Count pending items - */ - static async countPending(filter = {}) { - const collection = await getCollection('moderation_queue'); - return await collection.countDocuments({ - ...filter, - status: 'pending' - }); - } - - /** - * Get stats by quadrant - */ - static async getStatsByQuadrant() { - const collection = await getCollection('moderation_queue'); - - return await collection.aggregate([ - { $match: { status: 'pending' } }, - { - $group: { - _id: '$quadrant', - count: { $sum: 1 }, - high_priority: { - $sum: { $cond: [{ $eq: ['$priority', 'high'] }, 1, 0] } - } - } - } - ]).toArray(); - } - - /** - * Delete item - */ - static async delete(id) { - const collection = await getCollection('moderation_queue'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } -} - -module.exports = ModerationQueue; diff --git a/src/models/NewsletterSubscription.model.js b/src/models/NewsletterSubscription.model.js deleted file mode 100644 index 500eb80d..00000000 --- a/src/models/NewsletterSubscription.model.js +++ /dev/null @@ -1,235 +0,0 @@ -/** - * Newsletter Subscription Model - * Manages email subscriptions for blog updates and framework news - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class NewsletterSubscription { - /** - * Subscribe a new email address - */ - static async subscribe(data) { - const collection = await getCollection('newsletter_subscriptions'); - - // Check if already subscribed - const existing = await collection.findOne({ - email: data.email.toLowerCase() - }); - - if (existing) { - // If previously unsubscribed, reactivate - if (!existing.active) { - await collection.updateOne( - { _id: existing._id }, - { - $set: { - active: true, - resubscribed_at: new Date(), - updated_at: new Date() - } - } - ); - return { ...existing, active: true }; - } - // Already subscribed and active - return existing; - } - - // Create new subscription - const subscription = { - email: data.email.toLowerCase(), - name: data.name || null, - source: data.source || 'blog', // blog/homepage/docs/etc - interests: data.interests || [], // e.g., ['framework-updates', 'case-studies', 'research'] - verification_token: this._generateToken(), - verified: false, // Will be true after email verification - active: true, - subscribed_at: new Date(), - created_at: new Date(), - updated_at: new Date(), - metadata: { - ip_address: data.ip_address || null, - user_agent: data.user_agent || null, - referrer: data.referrer || null - } - }; - - const result = await collection.insertOne(subscription); - return { ...subscription, _id: result.insertedId }; - } - - /** - * Verify email subscription - */ - static async verify(token) { - const collection = await getCollection('newsletter_subscriptions'); - - const result = await collection.updateOne( - { verification_token: token, active: true }, - { - $set: { - verified: true, - verified_at: new Date(), - updated_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Unsubscribe an email address - */ - static async unsubscribe(email, token = null) { - const collection = await getCollection('newsletter_subscriptions'); - - const filter = token - ? { verification_token: token } - : { email: email.toLowerCase() }; - - const result = await collection.updateOne( - filter, - { - $set: { - active: false, - unsubscribed_at: new Date(), - updated_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Find subscription by email - */ - static async findByEmail(email) { - const collection = await getCollection('newsletter_subscriptions'); - return await collection.findOne({ email: email.toLowerCase() }); - } - - /** - * Find subscription by ID - */ - static async findById(id) { - const collection = await getCollection('newsletter_subscriptions'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * List all subscriptions - */ - static async list(options = {}) { - const collection = await getCollection('newsletter_subscriptions'); - const { - limit = 100, - skip = 0, - active = true, - verified = null, - source = null - } = options; - - const filter = {}; - if (active !== null) filter.active = active; - if (verified !== null) filter.verified = verified; - if (source) filter.source = source; - - const subscriptions = await collection - .find(filter) - .sort({ created_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - - return subscriptions; - } - - /** - * Count subscriptions - */ - static async count(filter = {}) { - const collection = await getCollection('newsletter_subscriptions'); - return await collection.countDocuments(filter); - } - - /** - * Update subscription preferences - */ - static async updatePreferences(email, preferences) { - const collection = await getCollection('newsletter_subscriptions'); - - const result = await collection.updateOne( - { email: email.toLowerCase(), active: true }, - { - $set: { - interests: preferences.interests || [], - name: preferences.name || null, - updated_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Get subscription statistics - */ - static async getStats() { - const collection = await getCollection('newsletter_subscriptions'); - - const [ - totalSubscribers, - activeSubscribers, - verifiedSubscribers, - recentSubscribers - ] = await Promise.all([ - collection.countDocuments({}), - collection.countDocuments({ active: true }), - collection.countDocuments({ active: true, verified: true }), - collection.countDocuments({ - active: true, - created_at: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } - }) - ]); - - // Get source breakdown - const sourceBreakdown = await collection.aggregate([ - { $match: { active: true } }, - { $group: { _id: '$source', count: { $sum: 1 } } } - ]).toArray(); - - return { - total: totalSubscribers, - active: activeSubscribers, - verified: verifiedSubscribers, - recent_30_days: recentSubscribers, - by_source: sourceBreakdown.reduce((acc, item) => { - acc[item._id || 'unknown'] = item.count; - return acc; - }, {}) - }; - } - - /** - * Delete subscription (admin only) - */ - static async delete(id) { - const collection = await getCollection('newsletter_subscriptions'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } - - /** - * Generate verification token - */ - static _generateToken() { - return require('crypto').randomBytes(32).toString('hex'); - } -} - -module.exports = NewsletterSubscription; diff --git a/src/models/Resource.model.js b/src/models/Resource.model.js deleted file mode 100644 index 46a00ff4..00000000 --- a/src/models/Resource.model.js +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Resource Model - * Curated directory of aligned resources - */ - -const { ObjectId } = require('mongodb'); -const { getCollection } = require('../utils/db.util'); - -class Resource { - /** - * Create a new resource - */ - static async create(data) { - const collection = await getCollection('resources'); - - const resource = { - url: data.url, - title: data.title, - description: data.description, - category: data.category, // framework/tool/research/organization/educational - subcategory: data.subcategory, - alignment_score: data.alignment_score, // 0-1 alignment with Tractatus values - ai_analysis: { - summary: data.ai_analysis?.summary, - relevance: data.ai_analysis?.relevance, - quality_indicators: data.ai_analysis?.quality_indicators || [], - concerns: data.ai_analysis?.concerns || [], - claude_reasoning: data.ai_analysis?.claude_reasoning - }, - status: data.status || 'pending', // pending/approved/rejected - reviewed_by: data.reviewed_by, - reviewed_at: data.reviewed_at, - tags: data.tags || [], - featured: data.featured || false, - added_at: new Date(), - last_checked: new Date() - }; - - const result = await collection.insertOne(resource); - return { ...resource, _id: result.insertedId }; - } - - /** - * Find resource by ID - */ - static async findById(id) { - const collection = await getCollection('resources'); - return await collection.findOne({ _id: new ObjectId(id) }); - } - - /** - * Find resource by URL - */ - static async findByUrl(url) { - const collection = await getCollection('resources'); - return await collection.findOne({ url }); - } - - /** - * Find approved resources - */ - static async findApproved(options = {}) { - const collection = await getCollection('resources'); - const { limit = 50, skip = 0, category } = options; - - const filter = { status: 'approved' }; - if (category) filter.category = category; - - return await collection - .find(filter) - .sort({ alignment_score: -1, added_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find featured resources - */ - static async findFeatured(options = {}) { - const collection = await getCollection('resources'); - const { limit = 10 } = options; - - return await collection - .find({ status: 'approved', featured: true }) - .sort({ alignment_score: -1 }) - .limit(limit) - .toArray(); - } - - /** - * Find by category - */ - static async findByCategory(category, options = {}) { - const collection = await getCollection('resources'); - const { limit = 30, skip = 0 } = options; - - return await collection - .find({ category, status: 'approved' }) - .sort({ alignment_score: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - } - - /** - * Find high-alignment pending resources - */ - static async findHighAlignment(options = {}) { - const collection = await getCollection('resources'); - const { limit = 10 } = options; - - return await collection - .find({ - status: 'pending', - alignment_score: { $gte: 0.8 } - }) - .sort({ alignment_score: -1 }) - .limit(limit) - .toArray(); - } - - /** - * Update resource - */ - static async update(id, updates) { - const collection = await getCollection('resources'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: updates } - ); - - return result.modifiedCount > 0; - } - - /** - * Approve resource - */ - static async approve(id, reviewerId) { - const collection = await getCollection('resources'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - status: 'approved', - reviewed_by: reviewerId, - reviewed_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Reject resource - */ - static async reject(id, reviewerId) { - const collection = await getCollection('resources'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { - $set: { - status: 'rejected', - reviewed_by: reviewerId, - reviewed_at: new Date() - } - } - ); - - return result.modifiedCount > 0; - } - - /** - * Mark as featured - */ - static async setFeatured(id, featured = true) { - const collection = await getCollection('resources'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: { featured } } - ); - - return result.modifiedCount > 0; - } - - /** - * Update last checked timestamp - */ - static async updateLastChecked(id) { - const collection = await getCollection('resources'); - - await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: { last_checked: new Date() } } - ); - } - - /** - * Count by status - */ - static async countByStatus(status) { - const collection = await getCollection('resources'); - return await collection.countDocuments({ status }); - } - - /** - * Delete resource - */ - static async delete(id) { - const collection = await getCollection('resources'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } -} - -module.exports = Resource; diff --git a/src/models/User.model.js b/src/models/User.model.js deleted file mode 100644 index 01b20485..00000000 --- a/src/models/User.model.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * User Model - * Admin user accounts - */ - -const { ObjectId } = require('mongodb'); -const bcrypt = require('bcrypt'); -const { getCollection } = require('../utils/db.util'); - -class User { - /** - * Create a new user - */ - static async create(data) { - const collection = await getCollection('users'); - - // Hash password - const hashedPassword = await bcrypt.hash(data.password, 10); - - const user = { - email: data.email, - password: hashedPassword, - name: data.name, - role: data.role || 'admin', // admin/moderator/viewer - created_at: new Date(), - last_login: null, - active: data.active !== undefined ? data.active : true - }; - - const result = await collection.insertOne(user); - - // Return user without password - const { password, ...userWithoutPassword } = { ...user, _id: result.insertedId }; - return userWithoutPassword; - } - - /** - * Find user by ID - */ - static async findById(id) { - const collection = await getCollection('users'); - const user = await collection.findOne({ _id: new ObjectId(id) }); - - if (user) { - const { password, ...userWithoutPassword } = user; - return userWithoutPassword; - } - return null; - } - - /** - * Find user by email - */ - static async findByEmail(email) { - const collection = await getCollection('users'); - return await collection.findOne({ email: email.toLowerCase() }); - } - - /** - * Authenticate user - */ - static async authenticate(email, password) { - const user = await this.findByEmail(email); - - if (!user || !user.active) { - return null; - } - - const isValid = await bcrypt.compare(password, user.password); - - if (!isValid) { - return null; - } - - // Update last login - await this.updateLastLogin(user._id); - - // Return user without password - const { password: _, ...userWithoutPassword } = user; - return userWithoutPassword; - } - - /** - * Update last login timestamp - */ - static async updateLastLogin(id) { - const collection = await getCollection('users'); - - await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: { last_login: new Date() } } - ); - } - - /** - * Update user - */ - static async update(id, updates) { - const collection = await getCollection('users'); - - // Remove password from updates (use changePassword for that) - const { password, ...safeUpdates } = updates; - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: safeUpdates } - ); - - return result.modifiedCount > 0; - } - - /** - * Change password - */ - static async changePassword(id, newPassword) { - const collection = await getCollection('users'); - - const hashedPassword = await bcrypt.hash(newPassword, 10); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: { password: hashedPassword } } - ); - - return result.modifiedCount > 0; - } - - /** - * Deactivate user - */ - static async deactivate(id) { - const collection = await getCollection('users'); - - const result = await collection.updateOne( - { _id: new ObjectId(id) }, - { $set: { active: false } } - ); - - return result.modifiedCount > 0; - } - - /** - * List all users - */ - static async list(options = {}) { - const collection = await getCollection('users'); - const { limit = 50, skip = 0 } = options; - - const users = await collection - .find({}, { projection: { password: 0 } }) - .sort({ created_at: -1 }) - .skip(skip) - .limit(limit) - .toArray(); - - return users; - } - - /** - * Count users - */ - static async count(filter = {}) { - const collection = await getCollection('users'); - return await collection.countDocuments(filter); - } - - /** - * Delete user - */ - static async delete(id) { - const collection = await getCollection('users'); - const result = await collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount > 0; - } -} - -module.exports = User; diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js deleted file mode 100644 index a22cc49f..00000000 --- a/src/routes/admin.routes.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Admin Routes - * Moderation queue and system management - */ - -const express = require('express'); -const router = express.Router(); - -const adminController = require('../controllers/admin.controller'); -const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); -const { validateRequired, validateObjectId } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); - -/** - * All admin routes require authentication - */ -router.use(authenticateToken); - -/** - * Moderation Queue - */ - -// GET /api/admin/moderation - List moderation queue items -router.get('/moderation', - requireRole('admin', 'moderator'), - asyncHandler(adminController.getModerationQueue) -); - -// GET /api/admin/moderation/:id - Get single moderation item -router.get('/moderation/:id', - requireRole('admin', 'moderator'), - validateObjectId('id'), - asyncHandler(adminController.getModerationItem) -); - -// POST /api/admin/moderation/:id/review - Review item (approve/reject/escalate) -router.post('/moderation/:id/review', - requireRole('admin', 'moderator'), - validateObjectId('id'), - validateRequired(['action']), - asyncHandler(adminController.reviewModerationItem) -); - -/** - * System Statistics - */ - -// GET /api/admin/stats - Get system statistics -router.get('/stats', - requireRole('admin', 'moderator'), - asyncHandler(adminController.getSystemStats) -); - -/** - * Activity Log - */ - -// GET /api/admin/activity - Get recent activity log -router.get('/activity', - requireRole('admin'), - asyncHandler(adminController.getActivityLog) -); - -module.exports = router; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js deleted file mode 100644 index 30fa04e4..00000000 --- a/src/routes/auth.routes.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Authentication Routes - */ - -const express = require('express'); -const rateLimit = require('express-rate-limit'); -const router = express.Router(); - -const authController = require('../controllers/auth.controller'); -const { authenticateToken } = require('../middleware/auth.middleware'); -const { validateEmail, validateRequired } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); - -// Rate limiter for login attempts (brute-force protection) -const loginLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, // 5 attempts per 15 minutes per IP - message: 'Too many login attempts from this IP. Please try again in 15 minutes.', - standardHeaders: true, - legacyHeaders: false, - skipSuccessfulRequests: false // Count successful logins too (prevents credential stuffing) -}); - -/** - * POST /api/auth/login - * Login with email and password - * Rate limited: 5 attempts per 15 minutes per IP - */ -router.post('/login', - loginLimiter, - validateRequired(['email', 'password']), - validateEmail('email'), - asyncHandler(authController.login) -); - -/** - * GET /api/auth/me - * Get current authenticated user - */ -router.get('/me', - authenticateToken, - asyncHandler(authController.getCurrentUser) -); - -/** - * POST /api/auth/logout - * Logout (logs the event, client removes token) - */ -router.post('/logout', - authenticateToken, - asyncHandler(authController.logout) -); - -module.exports = router; diff --git a/src/routes/blog.routes.js b/src/routes/blog.routes.js deleted file mode 100644 index c4da04ca..00000000 --- a/src/routes/blog.routes.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Blog Routes - * AI-curated blog endpoints - */ - -const express = require('express'); -const router = express.Router(); - -const blogController = require('../controllers/blog.controller'); -const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); -const { validateRequired, validateObjectId, validateSlug } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); - -/** - * Public routes - */ - -// GET /api/blog/rss - RSS feed (must be before /:slug to avoid conflict) -router.get('/rss', - asyncHandler(blogController.generateRSSFeed) -); - -// GET /api/blog - List published posts -router.get('/', - asyncHandler(blogController.listPublishedPosts) -); - -// GET /api/blog/:slug - Get published post by slug -router.get('/:slug', - asyncHandler(blogController.getPublishedPost) -); - -/** - * Admin routes - */ - -// POST /api/blog/suggest-topics - AI-powered topic suggestions (TRA-OPS-0002) -router.post('/suggest-topics', - authenticateToken, - requireRole('admin'), - validateRequired(['audience']), - asyncHandler(blogController.suggestTopics) -); - -// POST /api/blog/draft-post - AI-powered blog post drafting (TRA-OPS-0002) -// Enforces inst_016, inst_017, inst_018 -router.post('/draft-post', - authenticateToken, - requireRole('admin'), - validateRequired(['topic', 'audience']), - asyncHandler(blogController.draftBlogPost) -); - -// POST /api/blog/analyze-content - Analyze content for Tractatus compliance -router.post('/analyze-content', - authenticateToken, - requireRole('admin'), - validateRequired(['title', 'body']), - asyncHandler(blogController.analyzeContent) -); - -// GET /api/blog/editorial-guidelines - Get editorial guidelines -router.get('/editorial-guidelines', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(blogController.getEditorialGuidelines) -); - -// GET /api/blog/admin/posts?status=draft -router.get('/admin/posts', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(blogController.listPostsByStatus) -); - -// GET /api/blog/admin/:id - Get any post by ID -router.get('/admin/:id', - authenticateToken, - requireRole('admin', 'moderator'), - validateObjectId('id'), - asyncHandler(blogController.getPostById) -); - -// POST /api/blog - Create new post -router.post('/', - authenticateToken, - requireRole('admin'), - validateRequired(['title', 'slug', 'content']), - validateSlug, - asyncHandler(blogController.createPost) -); - -// PUT /api/blog/:id - Update post -router.put('/:id', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(blogController.updatePost) -); - -// POST /api/blog/:id/publish - Publish post -router.post('/:id/publish', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(blogController.publishPost) -); - -// DELETE /api/blog/:id - Delete post -router.delete('/:id', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(blogController.deletePost) -); - -module.exports = router; diff --git a/src/routes/cases.routes.js b/src/routes/cases.routes.js deleted file mode 100644 index 066aa35e..00000000 --- a/src/routes/cases.routes.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Case Study Routes - * Community case study submission endpoints - */ - -const express = require('express'); -const router = express.Router(); - -const casesController = require('../controllers/cases.controller'); -const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); -const { validateRequired, validateEmail, validateObjectId } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); -const { createInputValidationMiddleware } = require('../middleware/input-validation.middleware'); -const { formRateLimiter } = require('../middleware/rate-limit.middleware'); -const { csrfProtection } = require('../middleware/csrf-protection.middleware'); - -/** - * Public routes - */ - -// Validation schema for case study submission -const caseSubmissionSchema = { - 'submitter.name': { required: true, type: 'name', maxLength: 100 }, - 'submitter.email': { required: true, type: 'email', maxLength: 254 }, - 'submitter.organization': { required: false, type: 'default', maxLength: 200 }, - 'case_study.title': { required: true, type: 'title', maxLength: 200 }, - 'case_study.description': { required: true, type: 'description', maxLength: 50000 }, - 'case_study.failure_mode': { required: true, type: 'default', maxLength: 500 }, - 'case_study.context': { required: false, type: 'default', maxLength: 5000 }, - 'case_study.impact': { required: false, type: 'default', maxLength: 5000 }, - 'case_study.lessons_learned': { required: false, type: 'default', maxLength: 5000 } -}; - -// POST /api/cases/submit - Submit case study (public) -router.post('/submit', - formRateLimiter, // 5 requests per minute - csrfProtection, // CSRF validation - createInputValidationMiddleware(caseSubmissionSchema), - validateRequired([ - 'submitter.name', - 'submitter.email', - 'case_study.title', - 'case_study.description', - 'case_study.failure_mode' - ]), - validateEmail('submitter.email'), - asyncHandler(casesController.submitCase) -); - -/** - * Admin routes - */ - -// GET /api/cases/submissions/stats - Get submission statistics (admin) -router.get('/submissions/stats', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(casesController.getStats) -); - -// GET /api/cases/submissions - List all submissions (admin) -router.get('/submissions', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(casesController.listSubmissions) -); - -// GET /api/cases/submissions/high-relevance - List high-relevance pending (admin) -router.get('/submissions/high-relevance', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(casesController.listHighRelevance) -); - -// GET /api/cases/submissions/:id - Get submission by ID (admin) -router.get('/submissions/:id', - authenticateToken, - requireRole('admin', 'moderator'), - validateObjectId('id'), - asyncHandler(casesController.getSubmission) -); - -// POST /api/cases/submissions/:id/approve - Approve submission (admin) -router.post('/submissions/:id/approve', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(casesController.approveSubmission) -); - -// POST /api/cases/submissions/:id/reject - Reject submission (admin) -router.post('/submissions/:id/reject', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - validateRequired(['reason']), - asyncHandler(casesController.rejectSubmission) -); - -// POST /api/cases/submissions/:id/request-info - Request more information (admin) -router.post('/submissions/:id/request-info', - authenticateToken, - requireRole('admin', 'moderator'), - validateObjectId('id'), - validateRequired(['requested_info']), - asyncHandler(casesController.requestMoreInfo) -); - -// DELETE /api/cases/submissions/:id - Delete submission (admin) -router.delete('/submissions/:id', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(casesController.deleteSubmission) -); - -module.exports = router; diff --git a/src/routes/demo.routes.js b/src/routes/demo.routes.js deleted file mode 100644 index 23665aa5..00000000 --- a/src/routes/demo.routes.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Demo Routes - * Public API endpoints for interactive demos - * Rate-limited to prevent abuse - */ - -const express = require('express'); -const router = express.Router(); -const { asyncHandler } = require('../middleware/error.middleware'); - -// Import services -const { - classifier, - validator, - enforcer, - monitor -} = require('../services'); - -// Simple in-memory rate limiting for demos -const rateLimiter = new Map(); -const RATE_LIMIT = 20; // requests per minute -const RATE_WINDOW = 60000; // 1 minute - -function checkRateLimit(ip) { - const now = Date.now(); - const userRequests = rateLimiter.get(ip) || []; - - // Remove expired requests - const validRequests = userRequests.filter(time => now - time < RATE_WINDOW); - - if (validRequests.length >= RATE_LIMIT) { - return false; - } - - validRequests.push(now); - rateLimiter.set(ip, validRequests); - return true; -} - -// Rate limiting middleware -const demoRateLimit = (req, res, next) => { - const ip = req.ip || req.connection.remoteAddress; - - if (!checkRateLimit(ip)) { - return res.status(429).json({ - error: 'Too Many Requests', - message: 'Rate limit exceeded. Please try again in a minute.', - retryAfter: 60 - }); - } - - next(); -}; - -/** - * POST /api/demo/classify - * Public instruction classification for demo - */ -router.post('/classify', - demoRateLimit, - asyncHandler(async (req, res) => { - const { instruction } = req.body; - - if (!instruction || typeof instruction !== 'string') { - return res.status(400).json({ - error: 'Bad Request', - message: 'instruction field is required and must be a string' - }); - } - - if (instruction.length > 500) { - return res.status(400).json({ - error: 'Bad Request', - message: 'instruction must be 500 characters or less' - }); - } - - const classification = classifier.classify({ - text: instruction, - context: {}, - timestamp: new Date(), - source: 'demo' - }); - - res.json({ - success: true, - classification: { - quadrant: classification.quadrant, - persistence: classification.persistence, - temporal_scope: classification.temporal_scope || 'session', - verification_required: classification.verification_required || 'MANDATORY', - explicitness: classification.explicitness || 0.7, - human_oversight: classification.human_oversight || 'RECOMMENDED', - reasoning: classification.reasoning || generateReasoning(classification) - } - }); - }) -); - -/** - * POST /api/demo/boundary-check - * Public boundary enforcement check for demo - */ -router.post('/boundary-check', - demoRateLimit, - asyncHandler(async (req, res) => { - const { decision, description } = req.body; - - if (!decision || typeof decision !== 'string') { - return res.status(400).json({ - error: 'Bad Request', - message: 'decision field is required' - }); - } - - const action = { - type: 'decision', - decision: decision, - description: description || '', - timestamp: new Date() - }; - - const enforcement = enforcer.enforce(action, { source: 'demo' }); - - res.json({ - success: true, - enforcement: { - allowed: enforcement.allowed, - boundary_violated: enforcement.boundary_violated || null, - reasoning: enforcement.reasoning || generateBoundaryReasoning(enforcement, decision), - alternatives: enforcement.alternatives || [], - human_approval_required: !enforcement.allowed - } - }); - }) -); - -/** - * POST /api/demo/pressure-check - * Public pressure analysis for demo - */ -router.post('/pressure-check', - demoRateLimit, - asyncHandler(async (req, res) => { - const { tokens, messages, errors } = req.body; - - if (typeof tokens !== 'number' || typeof messages !== 'number') { - return res.status(400).json({ - error: 'Bad Request', - message: 'tokens and messages must be numbers' - }); - } - - const context = { - tokenUsage: tokens, - tokenBudget: 200000, - messageCount: messages, - errorCount: errors || 0, - source: 'demo' - }; - - const pressure = monitor.analyzePressure(context); - - res.json({ - success: true, - pressure: { - level: pressure.level, - score: pressure.score, - percentage: Math.round(pressure.score * 100), - recommendations: pressure.recommendations || generatePressureRecommendations(pressure), - factors: pressure.factors || {} - } - }); - }) -); - -/** - * Helper: Generate reasoning for classification - */ -function generateReasoning(classification) { - const { quadrant, persistence } = classification; - - const quadrantReasons = { - 'STRATEGIC': 'This appears to involve long-term values, mission, or organizational direction.', - 'OPERATIONAL': 'This relates to processes, policies, or project-level decisions.', - 'TACTICAL': 'This is an immediate implementation or action-level instruction.', - 'SYSTEM': 'This involves technical infrastructure or architectural decisions.', - 'STOCHASTIC': 'This relates to exploration, innovation, or experimentation.' - }; - - const persistenceReasons = { - 'HIGH': 'Should remain active for the duration of the project or longer.', - 'MEDIUM': 'Should remain active for this phase or session.', - 'LOW': 'Single-use or temporary instruction.', - 'VARIABLE': 'Applies conditionally based on context.' - }; - - return `${quadrantReasons[quadrant] || 'Classification based on instruction content.'} ${persistenceReasons[persistence] || ''}`; -} - -/** - * Helper: Generate boundary enforcement reasoning - */ -function generateBoundaryReasoning(enforcement, decision) { - if (enforcement.allowed) { - return 'This is a technical decision that can be automated with appropriate verification.'; - } - - const boundaryReasons = { - 'VALUES': 'Values decisions cannot be automated - they require human judgment.', - 'USER_AGENCY': 'Decisions affecting user agency require explicit consent.', - 'IRREVERSIBLE': 'Irreversible actions require human approval before execution.', - 'STRATEGIC': 'Strategic direction decisions must be made by humans.', - 'ETHICAL': 'Ethical considerations require human moral judgment.' - }; - - return boundaryReasons[enforcement.boundary_violated] || - 'This decision crosses into territory requiring human judgment.'; -} - -/** - * Helper: Generate pressure recommendations - */ -function generatePressureRecommendations(pressure) { - const { level, score } = pressure; - - if (level === 'NORMAL') { - return 'Operating normally. All systems green.'; - } else if (level === 'ELEVATED') { - return 'Elevated pressure detected. Increased verification recommended.'; - } else if (level === 'HIGH') { - return 'High pressure. Mandatory verification required for all actions.'; - } else if (level === 'CRITICAL') { - return 'Critical pressure! Recommend context refresh or session restart.'; - } else if (level === 'DANGEROUS') { - return 'DANGEROUS CONDITIONS. Human intervention required. Action execution blocked.'; - } - - return 'Pressure analysis complete.'; -} - -module.exports = router; diff --git a/src/routes/documents.routes.js b/src/routes/documents.routes.js deleted file mode 100644 index 1ef49c19..00000000 --- a/src/routes/documents.routes.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Documents Routes - * Framework documentation endpoints - */ - -const express = require('express'); -const router = express.Router(); - -const documentsController = require('../controllers/documents.controller'); -const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); -const { validateRequired, validateObjectId, validateSlug } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); - -/** - * Public routes (read-only) - */ - -// GET /api/documents/search?q=query -router.get('/search', - asyncHandler(documentsController.searchDocuments) -); - -// GET /api/documents/archived -router.get('/archived', - asyncHandler(documentsController.listArchivedDocuments) -); - -// GET /api/documents/drafts (admin only) -router.get('/drafts', - authenticateToken, - requireRole('admin'), - asyncHandler(documentsController.listDraftDocuments) -); - -// GET /api/documents -router.get('/', (req, res, next) => { - // Redirect browser requests to API documentation - const acceptsHtml = req.accepts('html'); - const acceptsJson = req.accepts('json'); - - if (acceptsHtml && !acceptsJson) { - return res.redirect(302, '/api-reference.html#documents'); - } - - next(); -}, asyncHandler(documentsController.listDocuments)); - -// GET /api/documents/:identifier (ID or slug) -router.get('/:identifier', - asyncHandler(documentsController.getDocument) -); - -/** - * Admin routes (protected) - */ - -// POST /api/documents -router.post('/', - authenticateToken, - requireRole('admin'), - validateRequired(['title', 'slug', 'quadrant', 'content_markdown']), - validateSlug, - asyncHandler(documentsController.createDocument) -); - -// PUT /api/documents/:id -router.put('/:id', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(documentsController.updateDocument) -); - -// DELETE /api/documents/:id -router.delete('/:id', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(documentsController.deleteDocument) -); - -// POST /api/documents/:id/publish (admin only) -// SECURITY: Explicit publish workflow with validation -router.post('/:id/publish', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - validateRequired(['category']), - asyncHandler(documentsController.publishDocument) -); - -// POST /api/documents/:id/unpublish (admin only) -router.post('/:id/unpublish', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(documentsController.unpublishDocument) -); - -module.exports = router; diff --git a/src/routes/koha.routes.js b/src/routes/koha.routes.js deleted file mode 100644 index c6140fc7..00000000 --- a/src/routes/koha.routes.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Koha Routes - * Donation system API endpoints - */ - -const express = require('express'); -const router = express.Router(); -const rateLimit = require('express-rate-limit'); -const kohaController = require('../controllers/koha.controller'); -const { authenticateToken, requireAdmin } = require('../middleware/auth.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); - -/** - * Rate limiting for donation endpoints - * More restrictive than general API limit to prevent abuse - */ -const donationLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - max: 10, // 10 requests per hour per IP - message: 'Too many donation attempts from this IP. Please try again in an hour.', - standardHeaders: true, - legacyHeaders: false, - // Skip rate limiting for webhook endpoint (Stripe needs reliable access) - skip: (req) => req.path === '/webhook' -}); - -/** - * Public routes - */ - -// Create checkout session for donation -// POST /api/koha/checkout -// Body: { amount, frequency, tier, donor: { name, email, country }, public_acknowledgement, public_name } -router.post('/checkout', donationLimiter, kohaController.createCheckout); - -// Stripe webhook endpoint -// POST /api/koha/webhook -// Note: Requires raw body, configured in app.js -router.post('/webhook', kohaController.handleWebhook); - -// Get public transparency metrics -// GET /api/koha/transparency -router.get('/transparency', kohaController.getTransparency); - -// Cancel recurring donation -// POST /api/koha/cancel -// Body: { subscriptionId, email } -// Rate limited to prevent abuse/guessing of subscription IDs -router.post('/cancel', donationLimiter, kohaController.cancelDonation); - -// Create Stripe Customer Portal session -// POST /api/koha/portal -// Body: { email } -// Rate limited to prevent abuse -router.post('/portal', donationLimiter, kohaController.createPortalSession); - -// Verify donation session (after Stripe redirect) -// GET /api/koha/verify/:sessionId -router.get('/verify/:sessionId', kohaController.verifySession); - -/** - * Admin-only routes - * Requires JWT authentication with admin role - */ - -// Get donation statistics -// GET /api/koha/statistics?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD -router.get('/statistics', - authenticateToken, - requireAdmin, - asyncHandler(kohaController.getStatistics) -); - -module.exports = router; diff --git a/src/routes/media.routes.js b/src/routes/media.routes.js deleted file mode 100644 index 4e66ad49..00000000 --- a/src/routes/media.routes.js +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Media Inquiry Routes - * Press/media inquiry submission and triage endpoints - */ - -const express = require('express'); -const router = express.Router(); - -const mediaController = require('../controllers/media.controller'); -const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); -const { validateRequired, validateEmail, validateObjectId } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); -const { createInputValidationMiddleware } = require('../middleware/input-validation.middleware'); -const { formRateLimiter } = require('../middleware/rate-limit.middleware'); -const { csrfProtection } = require('../middleware/csrf-protection.middleware'); - -/** - * Public routes - */ - -// Validation schema for media inquiry submission -const mediaInquirySchema = { - 'contact.name': { required: true, type: 'name', maxLength: 100 }, - 'contact.email': { required: true, type: 'email', maxLength: 254 }, - 'contact.outlet': { required: true, type: 'default', maxLength: 200 }, - 'contact.phone': { required: false, type: 'phone', maxLength: 20 }, - 'contact.role': { required: false, type: 'default', maxLength: 100 }, - 'inquiry.subject': { required: true, type: 'title', maxLength: 200 }, - 'inquiry.message': { required: true, type: 'description', maxLength: 5000 }, - 'inquiry.deadline': { required: false, type: 'default', maxLength: 100 } -}; - -// POST /api/media/inquiries - Submit media inquiry (public) -router.post('/inquiries', - formRateLimiter, // 5 requests per minute - csrfProtection, // CSRF validation - createInputValidationMiddleware(mediaInquirySchema), - validateRequired(['contact.name', 'contact.email', 'contact.outlet', 'inquiry.subject', 'inquiry.message']), - validateEmail('contact.email'), - asyncHandler(mediaController.submitInquiry) -); - -// GET /api/media/triage-stats - Get triage statistics (public, transparency) -router.get('/triage-stats', - asyncHandler(mediaController.getTriageStats) -); - -/** - * Admin routes - */ - -// GET /api/media/inquiries - List all inquiries (admin) -router.get('/inquiries', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(mediaController.listInquiries) -); - -// GET /api/media/inquiries/urgent - List high urgency inquiries (admin) -router.get('/inquiries/urgent', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(mediaController.listUrgentInquiries) -); - -// GET /api/media/inquiries/:id - Get inquiry by ID (admin) -router.get('/inquiries/:id', - authenticateToken, - requireRole('admin', 'moderator'), - validateObjectId('id'), - asyncHandler(mediaController.getInquiry) -); - -// POST /api/media/inquiries/:id/assign - Assign inquiry to user (admin) -router.post('/inquiries/:id/assign', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(mediaController.assignInquiry) -); - -// POST /api/media/inquiries/:id/triage - Run AI triage (admin) -router.post('/inquiries/:id/triage', - authenticateToken, - requireRole('admin', 'moderator'), - validateObjectId('id'), - asyncHandler(mediaController.triageInquiry) -); - -// POST /api/media/inquiries/:id/respond - Mark as responded (admin) -router.post('/inquiries/:id/respond', - authenticateToken, - requireRole('admin', 'moderator'), - validateObjectId('id'), - validateRequired(['content']), - asyncHandler(mediaController.respondToInquiry) -); - -// DELETE /api/media/inquiries/:id - Delete inquiry (admin) -router.delete('/inquiries/:id', - authenticateToken, - requireRole('admin'), - validateObjectId('id'), - asyncHandler(mediaController.deleteInquiry) -); - -module.exports = router; diff --git a/src/routes/newsletter.routes.js b/src/routes/newsletter.routes.js deleted file mode 100644 index f7557d13..00000000 --- a/src/routes/newsletter.routes.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Newsletter Routes - * Public subscription management and admin endpoints - */ - -const express = require('express'); -const router = express.Router(); - -const newsletterController = require('../controllers/newsletter.controller'); -const { authenticateToken, requireRole } = require('../middleware/auth.middleware'); -const { validateRequired } = require('../middleware/validation.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); -const { createInputValidationMiddleware } = require('../middleware/input-validation.middleware'); -const { formRateLimiter } = require('../middleware/rate-limit.middleware'); -const { csrfProtection } = require('../middleware/csrf-protection.middleware'); - -/** - * Public Routes - */ - -// Validation schema for newsletter subscription -const newsletterSubscribeSchema = { - 'email': { required: true, type: 'email', maxLength: 254 }, - 'name': { required: false, type: 'name', maxLength: 100 } -}; - -// POST /api/newsletter/subscribe - Subscribe to newsletter -router.post('/subscribe', - formRateLimiter, // 5 requests per minute - csrfProtection, // CSRF validation - createInputValidationMiddleware(newsletterSubscribeSchema), - validateRequired(['email']), - asyncHandler(newsletterController.subscribe) -); - -// GET /api/newsletter/verify/:token - Verify email subscription -router.get('/verify/:token', - asyncHandler(newsletterController.verify) -); - -// POST /api/newsletter/unsubscribe - Unsubscribe from newsletter -router.post('/unsubscribe', - asyncHandler(newsletterController.unsubscribe) -); - -// PUT /api/newsletter/preferences - Update subscription preferences -router.put('/preferences', - validateRequired(['email']), - asyncHandler(newsletterController.updatePreferences) -); - -/** - * Admin Routes (require authentication) - */ - -// GET /api/newsletter/admin/stats - Get newsletter statistics -router.get('/admin/stats', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(newsletterController.getStats) -); - -// GET /api/newsletter/admin/subscriptions - List all subscriptions -router.get('/admin/subscriptions', - authenticateToken, - requireRole('admin', 'moderator'), - asyncHandler(newsletterController.listSubscriptions) -); - -// GET /api/newsletter/admin/export - Export subscriptions as CSV -router.get('/admin/export', - authenticateToken, - requireRole('admin'), - asyncHandler(newsletterController.exportSubscriptions) -); - -// DELETE /api/newsletter/admin/subscriptions/:id - Delete subscription -router.delete('/admin/subscriptions/:id', - authenticateToken, - requireRole('admin'), - asyncHandler(newsletterController.deleteSubscription) -); - -module.exports = router; diff --git a/src/routes/test.routes.js b/src/routes/test.routes.js deleted file mode 100644 index cd9af46c..00000000 --- a/src/routes/test.routes.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Test Routes - * Development and testing endpoints - */ - -const express = require('express'); -const router = express.Router(); -const { createSecureUpload, ALLOWED_MIME_TYPES } = require('../middleware/file-security.middleware'); -const { asyncHandler } = require('../middleware/error.middleware'); -const logger = require('../utils/logger.util'); - -/** - * Test file upload endpoint - * POST /api/test/upload - * - * Tests the complete file security pipeline: - * - Multer upload - * - MIME type validation - * - Magic number validation - * - ClamAV malware scanning - * - Quarantine system - */ -router.post('/upload', - ...createSecureUpload({ - fileType: 'document', - maxFileSize: 10 * 1024 * 1024, // 10MB - allowedMimeTypes: ALLOWED_MIME_TYPES.document, - fieldName: 'file' - }), - asyncHandler(async (req, res) => { - if (!req.file) { - return res.status(400).json({ - error: 'Bad Request', - message: 'No file uploaded' - }); - } - - logger.info(`Test file upload successful: ${req.file.originalname}`); - - res.json({ - success: true, - message: 'File uploaded and validated successfully', - file: { - originalName: req.file.originalname, - filename: req.file.filename, - mimetype: req.file.mimetype, - size: req.file.size, - path: req.file.path - }, - security: { - mimeValidated: true, - malwareScan: 'passed', - quarantined: false - } - }); - }) -); - -/** - * Get upload statistics - * GET /api/test/upload-stats - */ -router.get('/upload-stats', - asyncHandler(async (req, res) => { - const fs = require('fs').promises; - const path = require('path'); - - try { - const uploadDir = process.env.UPLOAD_DIR || '/tmp/tractatus-uploads'; - const quarantineDir = process.env.QUARANTINE_DIR || '/var/quarantine/tractatus'; - - const uploadFiles = await fs.readdir(uploadDir).catch(() => []); - const quarantineFiles = await fs.readdir(quarantineDir).catch(() => []); - - // Get quarantine details - const quarantineDetails = []; - for (const file of quarantineFiles) { - if (file.endsWith('.json')) { - const metadataPath = path.join(quarantineDir, file); - const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8')); - quarantineDetails.push(metadata); - } - } - - res.json({ - success: true, - stats: { - uploads: { - directory: uploadDir, - count: uploadFiles.length, - files: uploadFiles - }, - quarantine: { - directory: quarantineDir, - count: Math.floor(quarantineFiles.length / 2), // Each quarantined file has .json metadata - items: quarantineDetails - } - } - }); - } catch (error) { - logger.error('Upload stats error:', error); - res.status(500).json({ - error: 'Internal Server Error', - message: 'Failed to retrieve upload statistics' - }); - } - }) -); - -module.exports = router; diff --git a/src/services/AdaptiveCommunicationOrchestrator.service.js b/src/services/AdaptiveCommunicationOrchestrator.service.js deleted file mode 100644 index e5aa1185..00000000 --- a/src/services/AdaptiveCommunicationOrchestrator.service.js +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright 2025 John G Stroh - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Adaptive Communication Orchestrator Service - * Prevents linguistic hierarchy in pluralistic deliberation - * - * Support Service for PluralisticDeliberationOrchestrator - * - * Implements: - * - inst_029: Adaptive Communication Tone (detect and mirror stakeholder style) - * - inst_030: Anti-Patronizing Language Filter (blocks condescending terms) - * - inst_031: Regional Communication Norms (Australian/NZ, Japanese, Māori protocols) - * - inst_032: Multilingual Engagement Protocol (language barrier accommodation) - * - * Core Principle: - * If Tractatus facilitates "non-hierarchical deliberation" but only communicates - * in formal academic English, it imposes Western liberal norms and excludes - * non-academics, non-English speakers, and working-class communities. - * - * Solution: Same deliberation outcome, culturally appropriate communication. - */ - -const logger = require('../utils/logger.util'); - -/** - * Communication style profiles - */ -const COMMUNICATION_STYLES = { - FORMAL_ACADEMIC: { - name: 'Formal Academic', - characteristics: ['citations', 'technical terms', 'formal register', 'hedging'], - tone: 'formal', - example: 'Thank you for your principled contribution grounded in privacy rights theory.' - }, - CASUAL_DIRECT: { - name: 'Casual Direct (Australian/NZ)', - characteristics: ['directness', 'anti-tall-poppy', 'informal', 'pragmatic'], - tone: 'casual', - pub_test: true, // Would this sound awkward in casual pub conversation? - example: 'Right, here\'s where we landed: Save lives first, but only when it\'s genuinely urgent.' - }, - MAORI_PROTOCOL: { - name: 'Te Reo Māori Protocol', - characteristics: ['mihi', 'whanaungatanga', 'collective framing', 'taonga respect'], - tone: 'respectful', - cultural_elements: ['kia ora', 'ngā mihi', 'whānau', 'kōrero', 'whakaaro', 'kei te pai'], - example: 'Kia ora [Name]. Ngā mihi for bringing the voice of your whānau to this kōrero.' - }, - JAPANESE_FORMAL: { - name: 'Japanese Formal (Honne/Tatemae aware)', - characteristics: ['indirect', 'high context', 'relationship-focused', 'face-saving'], - tone: 'formal', - cultural_concepts: ['honne', 'tatemae', 'wa', 'uchi/soto'], - example: 'We have carefully considered your valued perspective in reaching this decision.' - }, - PLAIN_LANGUAGE: { - name: 'Plain Language', - characteristics: ['simple', 'clear', 'accessible', 'non-jargon'], - tone: 'neutral', - example: 'We decided to prioritize safety in this case. Here\'s why...' - } -}; - -/** - * Patronizing terms to filter (inst_030) - */ -const PATRONIZING_PATTERNS = [ - { pattern: /\bsimply\b/gi, reason: 'Implies task is trivial' }, - { pattern: /\bobviously\b/gi, reason: 'Dismisses difficulty' }, - { pattern: /\bclearly\b/gi, reason: 'Assumes shared understanding' }, - { pattern: /\bas you may know\b/gi, reason: 'Condescending hedge' }, - { pattern: /\bof course\b/gi, reason: 'Assumes obviousness' }, - { pattern: /\bjust\b(?= (do|make|use|try))/gi, reason: 'Minimizes complexity' }, - { pattern: /\bbasically\b/gi, reason: 'Can be condescending' } -]; - -/** - * Language detection patterns (basic - production would use proper i18n library) - */ -const LANGUAGE_PATTERNS = { - 'te-reo-maori': /\b(kia ora|ngā mihi|whānau|kōrero|aroha|mana|taonga)\b/i, - 'japanese': /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/, - 'spanish': /\b(hola|gracias|por favor|señor|señora)\b/i, - 'french': /\b(bonjour|merci|monsieur|madame)\b/i, - 'german': /\b(guten tag|danke|herr|frau)\b/i -}; - -class AdaptiveCommunicationOrchestrator { - constructor() { - this.styles = COMMUNICATION_STYLES; - this.patronizingPatterns = PATRONIZING_PATTERNS; - this.languagePatterns = LANGUAGE_PATTERNS; - - // Statistics tracking - this.stats = { - total_adaptations: 0, - by_style: { - FORMAL_ACADEMIC: 0, - CASUAL_DIRECT: 0, - MAORI_PROTOCOL: 0, - JAPANESE_FORMAL: 0, - PLAIN_LANGUAGE: 0 - }, - patronizing_terms_removed: 0, - languages_detected: {} - }; - - logger.info('AdaptiveCommunicationOrchestrator initialized'); - } - - /** - * Adapt communication to target audience style - * @param {String} message - The message to adapt - * @param {Object} context - Audience context - * @returns {String} Adapted message - */ - adaptCommunication(message, context = {}) { - try { - let adaptedMessage = message; - - // 1. Detect input language (inst_032) - const detectedLanguage = this._detectLanguage(message); - if (detectedLanguage && detectedLanguage !== 'english') { - logger.info('Non-English language detected', { language: detectedLanguage }); - // In production, would trigger translation workflow - } - - // 2. Apply anti-patronizing filter (inst_030) - adaptedMessage = this._removePatronizingLanguage(adaptedMessage); - - // 3. Adapt to target communication style (inst_029) - const targetStyle = context.audience || 'PLAIN_LANGUAGE'; - adaptedMessage = this._adaptToStyle(adaptedMessage, targetStyle, context); - - // 4. Apply regional/cultural adaptations (inst_031) - if (context.cultural_context) { - adaptedMessage = this._applyCulturalContext(adaptedMessage, context.cultural_context); - } - - this.stats.total_adaptations++; - if (this.stats.by_style[targetStyle] !== undefined) { - this.stats.by_style[targetStyle]++; - } - - return adaptedMessage; - - } catch (error) { - logger.error('Communication adaptation error:', error); - // Fallback: return original message - return message; - } - } - - /** - * Check if message passes pub test (inst_029) - * Would this sound awkward in casual Australian/NZ pub conversation? - * @param {String} message - The message to check - * @returns {Object} Pub test result - */ - pubTest(message) { - const awkwardIndicators = [ - { pattern: /\bhereby\b/gi, reason: 'Too formal/legal' }, - { pattern: /\bforthwith\b/gi, reason: 'Archaic formal' }, - { pattern: /\bnotwithstanding\b/gi, reason: 'Unnecessarily complex' }, - { pattern: /\bpursuant to\b/gi, reason: 'Overly legal' }, - { pattern: /\bin accordance with\b/gi, reason: 'Bureaucratic' }, - { pattern: /\bas per\b/gi, reason: 'Business jargon' } - ]; - - const violations = []; - - for (const indicator of awkwardIndicators) { - const matches = message.match(indicator.pattern); - if (matches) { - violations.push({ - term: matches[0], - reason: indicator.reason, - suggestion: 'Use simpler, conversational language' - }); - } - } - - return { - passes: violations.length === 0, - violations, - message: violations.length === 0 - ? 'Message passes pub test - sounds natural in casual conversation' - : 'Message would sound awkward in casual pub conversation' - }; - } - - /** - * Detect communication style from incoming message - * @param {String} message - The message to analyze - * @returns {String} Detected style key - */ - detectStyle(message) { - const lowerMessage = message.toLowerCase(); - - // Check for Māori protocol indicators - if (this.languagePatterns['te-reo-maori'].test(message)) { - return 'MAORI_PROTOCOL'; - } - - // Check for formal academic indicators - const formalIndicators = ['furthermore', 'notwithstanding', 'pursuant to', 'hereby', 'therefore']; - const formalCount = formalIndicators.filter(term => lowerMessage.includes(term)).length; - if (formalCount >= 2) { - return 'FORMAL_ACADEMIC'; - } - - // Check for casual/direct indicators - const casualIndicators = ['right,', 'ok,', 'yeah,', 'fair?', 'reckon', 'mate']; - const casualCount = casualIndicators.filter(term => lowerMessage.includes(term)).length; - if (casualCount >= 1) { - return 'CASUAL_DIRECT'; - } - - // Default to plain language - return 'PLAIN_LANGUAGE'; - } - - /** - * Generate culturally-adapted greeting - * @param {String} recipientName - Name of recipient - * @param {Object} context - Cultural context - * @returns {String} Appropriate greeting - */ - generateGreeting(recipientName, context = {}) { - const style = context.communication_style || 'PLAIN_LANGUAGE'; - - switch (style) { - case 'MAORI_PROTOCOL': - return `Kia ora ${recipientName}`; - case 'JAPANESE_FORMAL': - return `${recipientName}様、いつもお世話になっております。`; // Formal Japanese greeting - case 'CASUAL_DIRECT': - return `Hi ${recipientName}`; - case 'FORMAL_ACADEMIC': - return `Dear ${recipientName},`; - default: - return `Hello ${recipientName}`; - } - } - - /** - * Private helper methods - */ - - _detectLanguage(message) { - for (const [language, pattern] of Object.entries(this.languagePatterns)) { - if (pattern.test(message)) { - // Track language detection - if (!this.stats.languages_detected[language]) { - this.stats.languages_detected[language] = 0; - } - this.stats.languages_detected[language]++; - - return language; - } - } - return 'english'; // Default assumption - } - - _removePatronizingLanguage(message) { - let cleaned = message; - let removedCount = 0; - - for (const { pattern, reason } of this.patronizingPatterns) { - const beforeLength = cleaned.length; - cleaned = cleaned.replace(pattern, ''); - const afterLength = cleaned.length; - - if (beforeLength !== afterLength) { - removedCount++; - logger.debug('Removed patronizing term', { reason }); - } - } - - if (removedCount > 0) { - this.stats.patronizing_terms_removed += removedCount; - // Clean up extra spaces left by removals - cleaned = cleaned.replace(/\s{2,}/g, ' ').trim(); - } - - return cleaned; - } - - _adaptToStyle(message, styleKey, context) { - const style = this.styles[styleKey]; - - if (!style) { - logger.warn('Unknown communication style', { styleKey }); - return message; - } - - // Style-specific adaptations - switch (styleKey) { - case 'FORMAL_ACADEMIC': - return this._adaptToFormalAcademic(message, context); - case 'CASUAL_DIRECT': - return this._adaptToCasualDirect(message, context); - case 'MAORI_PROTOCOL': - return this._adaptToMaoriProtocol(message, context); - case 'JAPANESE_FORMAL': - return this._adaptToJapaneseFormal(message, context); - case 'PLAIN_LANGUAGE': - return this._adaptToPlainLanguage(message, context); - default: - return message; - } - } - - _adaptToFormalAcademic(message, context) { - // Add formal register, hedge appropriately - // (In production, would use NLP transformation) - return message; - } - - _adaptToCasualDirect(message, context) { - // Remove unnecessary formality, make direct - let adapted = message; - - // Replace formal phrases with casual equivalents - const replacements = [ - { formal: /I would like to inform you that/gi, casual: 'Just so you know,' }, - { formal: /It is important to note that/gi, casual: 'Key thing:' }, - { formal: /We have determined that/gi, casual: 'We figured' }, - { formal: /In accordance with/gi, casual: 'Following' } - ]; - - for (const { formal, casual } of replacements) { - adapted = adapted.replace(formal, casual); - } - - return adapted; - } - - _adaptToMaoriProtocol(message, context) { - // Add appropriate te reo Māori protocol elements - // (In production, would consult with Māori language experts) - return message; - } - - _adaptToJapaneseFormal(message, context) { - // Add appropriate Japanese formal register elements - // (In production, would use Japanese language processing) - return message; - } - - _adaptToPlainLanguage(message, context) { - // Simplify jargon, use clear language - let adapted = message; - - // Replace jargon with plain equivalents - const jargonReplacements = [ - { jargon: /utilize/gi, plain: 'use' }, - { jargon: /facilitate/gi, plain: 'help' }, - { jargon: /implement/gi, plain: 'do' }, - { jargon: /endeavor/gi, plain: 'try' } - ]; - - for (const { jargon, plain } of jargonReplacements) { - adapted = adapted.replace(jargon, plain); - } - - return adapted; - } - - _applyCulturalContext(message, culturalContext) { - // Apply cultural adaptations based on context - // (In production, would be much more sophisticated) - return message; - } - - /** - * Get communication adaptation statistics - * @returns {Object} Statistics object - */ - getStats() { - return { - ...this.stats, - timestamp: new Date() - }; - } -} - -// Singleton instance -const orchestrator = new AdaptiveCommunicationOrchestrator(); - -// Export both singleton (default) and class (for testing) -module.exports = orchestrator; -module.exports.AdaptiveCommunicationOrchestrator = AdaptiveCommunicationOrchestrator; diff --git a/src/services/BlogCuration.service.js b/src/services/BlogCuration.service.js deleted file mode 100644 index bf77ecf4..00000000 --- a/src/services/BlogCuration.service.js +++ /dev/null @@ -1,609 +0,0 @@ -/** - * Blog Curation Service - * - * AI-assisted blog content curation with mandatory human oversight. - * Implements Tractatus framework boundary enforcement for content generation. - * - * Governance: TRA-OPS-0002 (AI suggests, human decides) - * Boundary Rules: - * - inst_016: NEVER fabricate statistics or make unverifiable claims - * - inst_017: NEVER use absolute assurance terms (guarantee, ensures 100%, etc.) - * - inst_018: NEVER claim production-ready status without evidence - * - * All AI-generated content MUST be reviewed and approved by a human before publication. - */ - -const claudeAPI = require('./ClaudeAPI.service'); -const BoundaryEnforcer = require('./BoundaryEnforcer.service'); -const { getMemoryProxy } = require('./MemoryProxy.service'); -const logger = require('../utils/logger.util'); - -class BlogCurationService { - constructor() { - // Initialize MemoryProxy for governance rule persistence and audit logging - this.memoryProxy = getMemoryProxy(); - this.enforcementRules = {}; // Will load inst_016, inst_017, inst_018 - this.memoryProxyInitialized = false; - - // Editorial guidelines - core principles for blog content - this.editorialGuidelines = { - tone: 'Professional, informative, evidence-based', - voice: 'Third-person objective (AI safety framework documentation)', - style: 'Clear, accessible technical writing', - principles: [ - 'Transparency: Cite sources for all claims', - 'Honesty: Acknowledge limitations and unknowns', - 'Evidence: No fabricated statistics or unverifiable claims', - 'Humility: No absolute guarantees or 100% assurances', - 'Accuracy: Production status claims must have evidence' - ], - forbiddenPatterns: [ - 'Fabricated statistics without sources', - 'Absolute terms: guarantee, ensures 100%, never fails, always works', - 'Unverified production claims: battle-tested (without evidence), industry-standard (without adoption metrics)', - 'Emotional manipulation or fear-mongering', - 'Misleading comparisons or false dichotomies' - ], - targetWordCounts: { - short: '600-900 words', - medium: '1000-1500 words', - long: '1800-2500 words' - } - }; - } - - /** - * Initialize MemoryProxy and load enforcement rules - * @returns {Promise} Initialization result - */ - async initialize() { - try { - await this.memoryProxy.initialize(); - - // Load critical enforcement rules from memory - const criticalRuleIds = ['inst_016', 'inst_017', 'inst_018']; - let rulesLoaded = 0; - - for (const ruleId of criticalRuleIds) { - const rule = await this.memoryProxy.getRule(ruleId); - if (rule) { - this.enforcementRules[ruleId] = rule; - rulesLoaded++; - } else { - logger.warn(`[BlogCuration] Enforcement rule ${ruleId} not found in memory`); - } - } - - this.memoryProxyInitialized = true; - - logger.info('[BlogCuration] MemoryProxy initialized', { - rulesLoaded, - totalCriticalRules: criticalRuleIds.length - }); - - return { - success: true, - rulesLoaded, - enforcementRules: Object.keys(this.enforcementRules) - }; - - } catch (error) { - logger.error('[BlogCuration] Failed to initialize MemoryProxy', { - error: error.message - }); - // Continue with existing validation logic even if memory fails - return { - success: false, - error: error.message, - rulesLoaded: 0 - }; - } - } - - /** - * Draft a full blog post using AI - * - * @param {Object} params - Blog post parameters - * @param {string} params.topic - Blog post topic/title - * @param {string} params.audience - Target audience (researcher/implementer/advocate) - * @param {string} params.length - Desired length (short/medium/long) - * @param {string} params.focus - Optional focus area or angle - * @returns {Promise} Draft blog post with metadata - */ - async draftBlogPost(params) { - const { topic, audience, length = 'medium', focus } = params; - - logger.info(`[BlogCuration] Drafting blog post: "${topic}" for ${audience}`); - - // 1. Boundary check - content generation requires human oversight - const boundaryCheck = BoundaryEnforcer.enforce({ - description: 'Generate AI-drafted blog content for human review', - text: 'Blog post will be queued for mandatory human approval before publication', - classification: { quadrant: 'OPERATIONAL' }, - type: 'content_generation' - }); - - if (!boundaryCheck.allowed) { - logger.warn(`[BlogCuration] Boundary check failed: ${boundaryCheck.reasoning}`); - throw new Error(`Boundary violation: ${boundaryCheck.reasoning}`); - } - - // 2. Build system prompt with editorial guidelines and Tractatus constraints - const systemPrompt = this._buildSystemPrompt(audience); - - // 3. Build user prompt for blog post generation - const userPrompt = this._buildDraftPrompt(topic, audience, length, focus); - - // 4. Call Claude API - const messages = [{ role: 'user', content: userPrompt }]; - - try { - const response = await claudeAPI.sendMessage(messages, { - system: systemPrompt, - max_tokens: this._getMaxTokens(length), - temperature: 0.7 // Balanced creativity and consistency - }); - - const content = claudeAPI.extractJSON(response); - - // 5. Validate generated content against Tractatus principles - const validation = await this._validateContent(content); - - // 6. Return draft with validation results - return { - draft: content, - validation, - boundary_check: boundaryCheck, - metadata: { - generated_at: new Date(), - model: response.model, - usage: response.usage, - audience, - length, - requires_human_approval: true - } - }; - - } catch (error) { - logger.error('[BlogCuration] Draft generation failed:', error); - throw new Error(`Blog draft generation failed: ${error.message}`); - } - } - - /** - * Suggest blog topics based on audience and existing documents - * (Fetches documents from site as context for topic generation) - * - * @param {string} audience - Target audience - * @param {string} theme - Optional theme/focus - * @returns {Promise} Topic suggestions with metadata - */ - async suggestTopics(audience, theme = null) { - logger.info(`[BlogCuration] Suggesting topics: audience=${audience}, theme=${theme || 'general'}`); - - try { - // Fetch existing documents as context - const Document = require('../models/Document.model'); - const documents = await Document.list({ limit: 20, skip: 0 }); - - // Build context from document titles and summaries - const documentContext = documents.map(doc => ({ - title: doc.title, - slug: doc.slug, - summary: doc.summary || doc.description || '' - })); - - // Generate topics with document context - const systemPrompt = `You are a content strategist for the Tractatus AI Safety Framework. -Your role is to suggest blog post topics that educate audiences about AI safety through sovereignty, -transparency, harmlessness, and community principles. - -The framework prevents AI from making irreducible human decisions and requires human oversight -for all values-sensitive choices. - -EXISTING DOCUMENTS ON SITE: -${documentContext.map(d => `- ${d.title}: ${d.summary}`).join('\n')} - -Suggest topics that: -1. Complement existing content (don't duplicate) -2. Address gaps in current documentation -3. Provide practical insights for ${audience} audience -4. Maintain Tractatus principles (no fabricated stats, no absolute guarantees)`; - - const userPrompt = theme - ? `Based on the existing documents above, suggest 5-7 NEW blog post topics for ${audience} audience focused on: ${theme} - -For each topic, provide: -{ - "title": "compelling, specific title", - "rationale": "why this topic fills a gap or complements existing content", - "target_word_count": 800-1500, - "key_points": ["3-5 bullet points"], - "tractatus_angle": "how it relates to framework principles" -} - -Respond with JSON array.` - : `Based on the existing documents above, suggest 5-7 NEW blog post topics for ${audience} audience about the Tractatus AI Safety Framework. - -For each topic, provide: -{ - "title": "compelling, specific title", - "rationale": "why this topic fills a gap or complements existing content", - "target_word_count": 800-1500, - "key_points": ["3-5 bullet points"], - "tractatus_angle": "how it relates to framework principles" -} - -Respond with JSON array.`; - - const messages = [{ role: 'user', content: userPrompt }]; - - const response = await claudeAPI.sendMessage(messages, { - system: systemPrompt, - max_tokens: 2048 - }); - - const topics = claudeAPI.extractJSON(response); - - // Validate topics don't contain forbidden patterns - const validatedTopics = topics.map(topic => ({ - ...topic, - validation: this._validateTopicTitle(topic.title) - })); - - return validatedTopics; - - } catch (error) { - logger.error('[BlogCuration] Topic suggestion failed:', error); - throw new Error(`Topic suggestion failed: ${error.message}`); - } - } - - /** - * Analyze existing blog content for Tractatus compliance - * - * @param {Object} content - Blog post content {title, body} - * @returns {Promise} Compliance analysis - */ - async analyzeContentCompliance(content) { - const { title, body } = content; - - logger.info(`[BlogCuration] Analyzing content compliance: "${title}"`); - - const systemPrompt = `You are a Tractatus Framework compliance auditor. -Analyze content for violations of these principles: - -1. NEVER fabricate statistics or make unverifiable claims -2. NEVER use absolute assurance terms (guarantee, ensures 100%, never fails, always works) -3. NEVER claim production-ready status without concrete evidence -4. ALWAYS cite sources for statistics and claims -5. ALWAYS acknowledge limitations and unknowns - -Return JSON with compliance analysis.`; - - const userPrompt = `Analyze this blog post for Tractatus compliance: - -Title: ${title} - -Content: -${body} - -Respond with JSON: -{ - "compliant": true/false, - "violations": [ - { - "type": "FABRICATED_STAT|ABSOLUTE_CLAIM|UNVERIFIED_PRODUCTION|OTHER", - "severity": "HIGH|MEDIUM|LOW", - "excerpt": "problematic text snippet", - "reasoning": "why this violates principles", - "suggested_fix": "how to correct it" - } - ], - "warnings": ["..."], - "strengths": ["..."], - "overall_score": 0-100, - "recommendation": "PUBLISH|EDIT_REQUIRED|REJECT" -}`; - - const messages = [{ role: 'user', content: userPrompt }]; - - try { - const response = await claudeAPI.sendMessage(messages, { - system: systemPrompt, - max_tokens: 2048 - }); - - return claudeAPI.extractJSON(response); - - } catch (error) { - logger.error('[BlogCuration] Compliance analysis failed:', error); - throw new Error(`Compliance analysis failed: ${error.message}`); - } - } - - /** - * Generate SEO-friendly slug from title - * - * @param {string} title - Blog post title - * @returns {string} URL-safe slug - */ - generateSlug(title) { - return title - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, '') - .substring(0, 100); - } - - /** - * Extract excerpt from blog content - * - * @param {string} content - Full blog content (HTML or markdown) - * @param {number} maxLength - Maximum excerpt length (default 200) - * @returns {string} Excerpt - */ - extractExcerpt(content, maxLength = 200) { - // Strip HTML/markdown tags - const plainText = content - .replace(/<[^>]*>/g, '') - .replace(/[#*_`]/g, '') - .trim(); - - if (plainText.length <= maxLength) { - return plainText; - } - - // Find last complete sentence within maxLength - const excerpt = plainText.substring(0, maxLength); - const lastPeriod = excerpt.lastIndexOf('.'); - - if (lastPeriod > maxLength * 0.5) { - return excerpt.substring(0, lastPeriod + 1); - } - - return excerpt.substring(0, maxLength).trim() + '...'; - } - - /** - * Build system prompt with editorial guidelines - * @private - */ - _buildSystemPrompt(audience) { - const audienceContext = { - researcher: 'Academic researchers, AI safety specialists, technical analysts', - implementer: 'Software engineers, system architects, technical decision-makers', - advocate: 'Policy makers, ethicists, public stakeholders, non-technical audiences', - general: 'Mixed audience with varying technical backgrounds' - }; - - return `You are a professional technical writer creating content for the Tractatus AI Safety Framework blog. - -AUDIENCE: ${audienceContext[audience] || audienceContext.general} - -TRACTATUS FRAMEWORK CORE PRINCIPLES: -1. What cannot be systematized must not be automated -2. AI must never make irreducible human decisions -3. Sovereignty: User agency over values and goals -4. Transparency: Explicit instructions, audit trails, governance logs -5. Harmlessness: Boundary enforcement prevents values automation -6. Community: Open frameworks, shared governance patterns - -EDITORIAL GUIDELINES: -- Tone: ${this.editorialGuidelines.tone} -- Voice: ${this.editorialGuidelines.voice} -- Style: ${this.editorialGuidelines.style} - -MANDATORY CONSTRAINTS (inst_016, inst_017, inst_018): -${this.editorialGuidelines.principles.map(p => `- ${p}`).join('\n')} - -FORBIDDEN PATTERNS: -${this.editorialGuidelines.forbiddenPatterns.map(p => `- ${p}`).join('\n')} - -OUTPUT FORMAT: JSON with structure: -{ - "title": "SEO-friendly title (60 chars max)", - "subtitle": "Compelling subtitle (120 chars max)", - "content": "Full blog post content in Markdown format", - "excerpt": "Brief excerpt (150-200 chars)", - "tags": ["tag1", "tag2", "tag3"], - "tractatus_angle": "How this relates to framework principles", - "sources": ["URL or reference for claims made"], - "word_count": actual_word_count -}`; - } - - /** - * Build user prompt for blog post draft - * @private - */ - _buildDraftPrompt(topic, audience, length, focus) { - const wordCount = { - short: '600-900', - medium: '1000-1500', - long: '1800-2500' - }[length] || '1000-1500'; - - let prompt = `Write a blog post about: ${topic} - -Target word count: ${wordCount} words -Audience: ${audience}`; - - if (focus) { - prompt += `\nSpecific focus: ${focus}`; - } - - prompt += ` - -Requirements: -1. Evidence-based: Cite sources for all statistics and claims -2. Honest: Acknowledge limitations, unknowns, trade-offs -3. No fabricated data or unverifiable claims -4. No absolute guarantees or 100% assurances -5. Clear connection to Tractatus framework principles -6. Actionable insights or takeaways for the ${audience} audience -7. SEO-friendly structure with headers, lists, and clear sections - -Respond with JSON as specified in the system prompt.`; - - return prompt; - } - - /** - * Get max tokens based on target length - * @private - */ - _getMaxTokens(length) { - const tokenMap = { - short: 2048, - medium: 3072, - long: 4096 - }; - return tokenMap[length] || 3072; - } - - /** - * Validate content against Tractatus principles - * @private - */ - async _validateContent(content) { - const violations = []; - const warnings = []; - - const textToCheck = `${content.title} ${content.subtitle} ${content.content}`.toLowerCase(); - - // Check for forbidden patterns (inst_016, inst_017, inst_018) - const forbiddenTerms = { - absolute_guarantees: ['guarantee', 'guarantees', 'guaranteed', 'ensures 100%', 'never fails', 'always works', '100% safe', '100% secure'], - fabricated_stats: [], // Can't detect without external validation - unverified_production: ['battle-tested', 'production-proven', 'industry-standard'] - }; - - // Check absolute guarantees (inst_017) - forbiddenTerms.absolute_guarantees.forEach(term => { - if (textToCheck.includes(term)) { - violations.push({ - type: 'ABSOLUTE_GUARANTEE', - severity: 'HIGH', - term, - instruction: 'inst_017', - message: `Forbidden absolute assurance term: "${term}"` - }); - } - }); - - // Check unverified production claims (inst_018) - forbiddenTerms.unverified_production.forEach(term => { - if (textToCheck.includes(term) && (!content.sources || content.sources.length === 0)) { - warnings.push({ - type: 'UNVERIFIED_CLAIM', - severity: 'MEDIUM', - term, - instruction: 'inst_018', - message: `Production claim "${term}" requires citation` - }); - } - }); - - // Check for uncited statistics (inst_016) - const statPattern = /\d+(\.\d+)?%/g; - const statsFound = (content.content.match(statPattern) || []).length; - - if (statsFound > 0 && (!content.sources || content.sources.length === 0)) { - warnings.push({ - type: 'UNCITED_STATISTICS', - severity: 'HIGH', - instruction: 'inst_016', - message: `Found ${statsFound} statistics without sources - verify these are not fabricated` - }); - } - - const isValid = violations.length === 0; - - const validationResult = { - valid: isValid, - violations, - warnings, - stats_found: statsFound, - sources_provided: content.sources?.length || 0, - recommendation: violations.length > 0 ? 'REJECT' : - warnings.length > 0 ? 'REVIEW_REQUIRED' : - 'APPROVED' - }; - - // Audit validation decision - this._auditValidationDecision(content, validationResult); - - return validationResult; - } - - /** - * Audit content validation decision to memory (async, non-blocking) - * @private - */ - _auditValidationDecision(content, validationResult) { - // Only audit if MemoryProxy is initialized - if (!this.memoryProxyInitialized) { - return; - } - - // Extract violation instruction IDs - const violatedRules = [ - ...validationResult.violations.map(v => v.instruction), - ...validationResult.warnings.map(w => w.instruction) - ].filter(Boolean); - - // Audit asynchronously (don't block validation) - this.memoryProxy.auditDecision({ - sessionId: 'blog-curation-service', - action: 'content_validation', - rulesChecked: Object.keys(this.enforcementRules), - violations: violatedRules, - allowed: validationResult.valid, - metadata: { - content_title: content.title, - violation_count: validationResult.violations.length, - warning_count: validationResult.warnings.length, - stats_found: validationResult.stats_found, - sources_provided: validationResult.sources_provided, - recommendation: validationResult.recommendation - } - }).catch(error => { - logger.error('[BlogCuration] Failed to audit validation decision', { - error: error.message, - title: content.title - }); - }); - } - - /** - * Validate topic title for forbidden patterns - * @private - */ - _validateTopicTitle(title) { - const textToCheck = title.toLowerCase(); - const issues = []; - - // Check for absolute guarantees - if (textToCheck.match(/guarantee|100%|never fail|always work/)) { - issues.push('Contains absolute assurance language'); - } - - return { - valid: issues.length === 0, - issues - }; - } - - /** - * Get editorial guidelines (for display in admin UI) - * - * @returns {Object} Editorial guidelines - */ - getEditorialGuidelines() { - return this.editorialGuidelines; - } -} - -// Export singleton instance -module.exports = new BlogCurationService(); diff --git a/src/services/ClaudeAPI.service.js b/src/services/ClaudeAPI.service.js deleted file mode 100644 index 5a16bd10..00000000 --- a/src/services/ClaudeAPI.service.js +++ /dev/null @@ -1,425 +0,0 @@ -/** - * Claude API Service - * - * Provides interface to Anthropic's Claude API for AI-powered features. - * All AI operations go through this service to ensure consistent error handling, - * rate limiting, and governance compliance. - * - * Usage: - * const claudeAPI = require('./ClaudeAPI.service'); - * const response = await claudeAPI.sendMessage(messages, options); - */ - -const https = require('https'); - -class ClaudeAPIService { - constructor() { - this.apiKey = process.env.CLAUDE_API_KEY; - this.model = process.env.CLAUDE_MODEL || 'claude-sonnet-4-5-20250929'; - this.maxTokens = parseInt(process.env.CLAUDE_MAX_TOKENS) || 4096; - this.apiVersion = '2023-06-01'; - this.hostname = 'api.anthropic.com'; - - if (!this.apiKey) { - console.error('WARNING: CLAUDE_API_KEY not set in environment variables'); - } - } - - /** - * Send a message to Claude API - * - * @param {Array} messages - Array of message objects [{role: 'user', content: '...'}] - * @param {Object} options - Optional overrides (model, max_tokens, temperature) - * @returns {Promise} API response - */ - async sendMessage(messages, options = {}) { - if (!this.apiKey) { - throw new Error('Claude API key not configured'); - } - - const payload = { - model: options.model || this.model, - max_tokens: options.max_tokens || this.maxTokens, - messages: messages, - ...(options.system && { system: options.system }), - ...(options.temperature && { temperature: options.temperature }) - }; - - try { - const response = await this._makeRequest(payload); - - // Log usage for monitoring - if (response.usage) { - console.log(`[ClaudeAPI] Usage: ${response.usage.input_tokens} in, ${response.usage.output_tokens} out`); - } - - return response; - } catch (error) { - console.error('[ClaudeAPI] Error:', error.message); - throw error; - } - } - - /** - * Make HTTP request to Claude API - * - * @private - * @param {Object} payload - Request payload - * @returns {Promise} Parsed response - */ - _makeRequest(payload) { - return new Promise((resolve, reject) => { - const postData = JSON.stringify(payload); - - const options = { - hostname: this.hostname, - port: 443, - path: '/v1/messages', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': this.apiKey, - 'anthropic-version': this.apiVersion, - 'Content-Length': Buffer.byteLength(postData) - } - }; - - const req = https.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode === 200) { - try { - const response = JSON.parse(data); - resolve(response); - } catch (error) { - reject(new Error(`Failed to parse API response: ${error.message}`)); - } - } else { - reject(new Error(`API request failed with status ${res.statusCode}: ${data}`)); - } - }); - }); - - req.on('error', (error) => { - reject(new Error(`Request error: ${error.message}`)); - }); - - req.write(postData); - req.end(); - }); - } - - /** - * Extract text content from Claude API response - * - * @param {Object} response - Claude API response - * @returns {string} Extracted text content - */ - extractTextContent(response) { - if (!response || !response.content || !Array.isArray(response.content)) { - throw new Error('Invalid Claude API response format'); - } - - const textBlock = response.content.find(block => block.type === 'text'); - if (!textBlock) { - throw new Error('No text content in Claude API response'); - } - - return textBlock.text; - } - - /** - * Parse JSON from Claude response (handles markdown code blocks) - * - * @param {Object} response - Claude API response - * @returns {Object} Parsed JSON - */ - extractJSON(response) { - const text = this.extractTextContent(response); - - // Remove markdown code blocks if present - let jsonText = text.trim(); - if (jsonText.startsWith('```json')) { - jsonText = jsonText.replace(/^```json\n/, '').replace(/\n```$/, ''); - } else if (jsonText.startsWith('```')) { - jsonText = jsonText.replace(/^```\n/, '').replace(/\n```$/, ''); - } - - try { - return JSON.parse(jsonText); - } catch (error) { - throw new Error(`Failed to parse JSON from Claude response: ${error.message}\nText: ${jsonText}`); - } - } - - /** - * Classify an instruction into Tractatus quadrants - * - * @param {string} instructionText - The instruction to classify - * @returns {Promise} Classification result - */ - async classifyInstruction(instructionText) { - const messages = [{ - role: 'user', - content: `Classify the following instruction into one of these quadrants: STRATEGIC, OPERATIONAL, TACTICAL, SYSTEM, or STOCHASTIC. - -Instruction: "${instructionText}" - -Respond with JSON only: -{ - "quadrant": "...", - "persistence": "HIGH/MEDIUM/LOW", - "temporal_scope": "PROJECT/SESSION/TASK", - "verification_required": "MANDATORY/RECOMMENDED/NONE", - "explicitness": 0.0-1.0, - "reasoning": "brief explanation" -}` - }]; - - const response = await this.sendMessage(messages, { max_tokens: 1024 }); - return this.extractJSON(response); - } - - /** - * Generate blog topic suggestions - * - * @param {string} audience - Target audience (researcher/implementer/advocate) - * @param {string} theme - Optional theme or focus area - * @returns {Promise} Array of topic suggestions - */ - async generateBlogTopics(audience, theme = null) { - const systemPrompt = `You are a content strategist for the Tractatus AI Safety Framework. -Your role is to suggest blog post topics that educate audiences about AI safety through sovereignty, -transparency, harmlessness, and community principles. - -The framework prevents AI from making irreducible human decisions and requires human oversight -for all values-sensitive choices.`; - - const userPrompt = theme - ? `Suggest 5-7 blog post topics for ${audience} audience focused on: ${theme} - -For each topic, provide: -- Title (compelling, specific) -- Subtitle (1 sentence) -- Target word count (800-1500) -- Key points to cover (3-5 bullets) -- Tractatus angle (how it relates to framework) - -Respond with JSON array.` - : `Suggest 5-7 blog post topics for ${audience} audience about the Tractatus AI Safety Framework. - -For each topic, provide: -- Title (compelling, specific) -- Subtitle (1 sentence) -- Target word count (800-1500) -- Key points to cover (3-5 bullets) -- Tractatus angle (how it relates to framework) - -Respond with JSON array.`; - - const messages = [{ role: 'user', content: userPrompt }]; - - const response = await this.sendMessage(messages, { - system: systemPrompt, - max_tokens: 2048 - }); - - return this.extractJSON(response); - } - - /** - * Classify media inquiry by priority - * - * @param {Object} inquiry - Media inquiry object {outlet, request, deadline} - * @returns {Promise} Classification with priority and reasoning - */ - async classifyMediaInquiry(inquiry) { - const { outlet, request, deadline } = inquiry; - - const systemPrompt = `You are a media relations assistant for the Tractatus AI Safety Framework. -Classify media inquiries by priority (HIGH/MEDIUM/LOW) based on: -- Outlet credibility and reach -- Request type (interview, comment, feature) -- Deadline urgency -- Topic relevance to framework`; - - const userPrompt = `Classify this media inquiry: - -Outlet: ${outlet} -Request: ${request} -Deadline: ${deadline || 'Not specified'} - -Respond with JSON: -{ - "priority": "HIGH/MEDIUM/LOW", - "reasoning": "brief explanation", - "recommended_response_time": "hours or days", - "suggested_spokesperson": "technical expert, policy lead, or framework creator" -}`; - - const messages = [{ role: 'user', content: userPrompt }]; - - const response = await this.sendMessage(messages, { - system: systemPrompt, - max_tokens: 1024 - }); - - return this.extractJSON(response); - } - - /** - * Draft suggested response to media inquiry - * (ALWAYS requires human approval before sending - TRA-OPS-0003) - * - * @param {Object} inquiry - Media inquiry object - * @param {string} priority - Priority classification - * @returns {Promise} Draft response text - */ - async draftMediaResponse(inquiry, priority) { - const { outlet, request } = inquiry; - - const systemPrompt = `You are drafting a suggested response to a media inquiry about the Tractatus AI Safety Framework. - -IMPORTANT: This is a DRAFT only. A human will review and approve before sending. - -Framework Core Principles: -1. What cannot be systematized must not be automated -2. AI must never make irreducible human decisions -3. Sovereignty: User agency over values and goals -4. Transparency: Explicit instructions, audit trails -5. Harmlessness: Boundary enforcement prevents values automation -6. Community: Open frameworks, shared governance`; - - const userPrompt = `Draft a ${priority} priority response to: - -Outlet: ${outlet} -Request: ${request} - -Requirements: -- Professional, informative tone -- 150-250 words -- Offer specific value (interview, technical details, case studies) -- Mention framework website: agenticgovernance.digital -- Include contact for follow-up - -Respond with plain text (not JSON).`; - - const messages = [{ role: 'user', content: userPrompt }]; - - const response = await this.sendMessage(messages, { - system: systemPrompt, - max_tokens: 1024 - }); - - return this.extractTextContent(response); - } - - /** - * Analyze case study relevance - * - * @param {Object} caseStudy - Case study object {title, description, evidence} - * @returns {Promise} Analysis with relevance score - */ - async analyzeCaseRelevance(caseStudy) { - const { title, description, evidence } = caseStudy; - - const systemPrompt = `You are evaluating case study submissions for the Tractatus AI Safety Framework. - -Assess relevance based on: -1. Demonstrates framework principles (sovereignty, transparency, harmlessness) -2. Shows AI safety concerns addressed by Tractatus -3. Provides concrete evidence or examples -4. Offers insights valuable to community -5. Ethical considerations (privacy, consent, impact) - -Score 0-100 where: -- 80-100: Highly relevant, publish with minor edits -- 60-79: Relevant, needs some editing -- 40-59: Somewhat relevant, major editing needed -- 0-39: Not relevant or low quality`; - - const userPrompt = `Analyze this case study submission: - -Title: ${title} -Description: ${description} -Evidence: ${evidence || 'Not provided'} - -Respond with JSON: -{ - "relevance_score": 0-100, - "strengths": ["..."], - "weaknesses": ["..."], - "recommended_action": "PUBLISH/EDIT/REJECT", - "ethical_concerns": ["..."] or null, - "suggested_improvements": ["..."] -}`; - - const messages = [{ role: 'user', content: userPrompt }]; - - const response = await this.sendMessage(messages, { - system: systemPrompt, - max_tokens: 1536 - }); - - return this.extractJSON(response); - } - - /** - * Curate external resources (websites, papers, tools) - * - * @param {Object} resource - Resource object {url, title, description} - * @returns {Promise} Curation analysis - */ - async curateResource(resource) { - const { url, title, description } = resource; - - const systemPrompt = `You are curating external resources for the Tractatus AI Safety Framework resource directory. - -Evaluate based on: -1. Alignment with framework values (sovereignty, transparency, harmlessness) -2. Quality and credibility of source -3. Relevance to AI safety, governance, or ethics -4. Usefulness to target audiences (researchers, implementers, advocates) - -Categorize into: -- PAPERS: Academic research, technical documentation -- TOOLS: Software, frameworks, libraries -- ORGANIZATIONS: Aligned groups, communities -- STANDARDS: Regulatory frameworks, best practices -- ARTICLES: Blog posts, essays, commentaries`; - - const userPrompt = `Evaluate this resource for inclusion: - -URL: ${url} -Title: ${title} -Description: ${description} - -Respond with JSON: -{ - "recommended": true/false, - "category": "PAPERS/TOOLS/ORGANIZATIONS/STANDARDS/ARTICLES", - "alignment_score": 0-100, - "target_audience": ["researcher", "implementer", "advocate"], - "tags": ["..."], - "reasoning": "brief explanation", - "concerns": ["..."] or null -}`; - - const messages = [{ role: 'user', content: userPrompt }]; - - const response = await this.sendMessage(messages, { - system: systemPrompt, - max_tokens: 1024 - }); - - return this.extractJSON(response); - } -} - -// Export singleton instance -module.exports = new ClaudeAPIService(); diff --git a/src/services/ClaudeMdAnalyzer.service.js b/src/services/ClaudeMdAnalyzer.service.js deleted file mode 100644 index 5b6c5f00..00000000 --- a/src/services/ClaudeMdAnalyzer.service.js +++ /dev/null @@ -1,442 +0,0 @@ -/** - * CLAUDE.md Analyzer Service - * - * Parses CLAUDE.md files and extracts candidate governance rules. - * Classifies statements by quality and provides migration recommendations. - * - * Part of Phase 2: AI Rule Optimizer & CLAUDE.md Analyzer - */ - -const RuleOptimizer = require('./RuleOptimizer.service'); - -class ClaudeMdAnalyzer { - constructor() { - // Keywords for quadrant classification - this.quadrantKeywords = { - STRATEGIC: ['architecture', 'design', 'philosophy', 'approach', 'values', 'mission', 'vision', 'goal'], - OPERATIONAL: ['workflow', 'process', 'procedure', 'convention', 'standard', 'practice', 'guideline'], - TACTICAL: ['implementation', 'code', 'function', 'class', 'variable', 'syntax', 'pattern'], - SYSTEM: ['port', 'database', 'server', 'infrastructure', 'deployment', 'environment', 'service'], - STORAGE: ['state', 'session', 'cache', 'persistence', 'data', 'storage', 'memory'] - }; - - // Imperative indicators (for HIGH persistence) - this.imperatives = ['MUST', 'SHALL', 'REQUIRED', 'PROHIBITED', 'NEVER', 'ALWAYS', 'MANDATORY']; - - // Preference indicators (for MEDIUM persistence) - this.preferences = ['SHOULD', 'RECOMMENDED', 'PREFERRED', 'ENCOURAGED']; - - // Suggestion indicators (for LOW persistence) - this.suggestions = ['MAY', 'CAN', 'CONSIDER', 'TRY', 'MIGHT']; - } - - /** - * Parse CLAUDE.md content into structured sections - * - * @param {string} content - Raw CLAUDE.md content - * @returns {Object} Parsed structure with sections - */ - parse(content) { - const lines = content.split('\n'); - const sections = []; - let currentSection = null; - - lines.forEach((line, index) => { - // Detect headings (# or ##) - const headingMatch = line.match(/^(#{1,6})\s+(.+)/); - if (headingMatch) { - if (currentSection) { - sections.push(currentSection); - } - currentSection = { - level: headingMatch[1].length, - title: headingMatch[2].trim(), - content: [], - lineStart: index - }; - } else if (currentSection && line.trim()) { - currentSection.content.push(line.trim()); - } - }); - - if (currentSection) { - sections.push(currentSection); - } - - return { - totalLines: lines.length, - sections, - content: content - }; - } - - /** - * Extract candidate rules from parsed content - * - * @param {Object} parsedContent - Output from parse() - * @returns {Array} Array of candidate rules - */ - extractCandidateRules(parsedContent) { - const candidates = []; - - parsedContent.sections.forEach(section => { - section.content.forEach(statement => { - // Skip very short statements - if (statement.length < 15) return; - - // Detect if statement has imperative language - const hasImperative = this.imperatives.some(word => - new RegExp(`\\b${word}\\b`).test(statement) - ); - - const hasPreference = this.preferences.some(word => - new RegExp(`\\b${word}\\b`, 'i').test(statement) - ); - - const hasSuggestion = this.suggestions.some(word => - new RegExp(`\\b${word}\\b`, 'i').test(statement) - ); - - // Only process statements with governance language - if (!hasImperative && !hasPreference && !hasSuggestion) { - return; - } - - // Classify quadrant based on keywords - const quadrant = this._classifyQuadrant(statement); - - // Classify persistence based on language strength - let persistence = 'LOW'; - if (hasImperative) persistence = 'HIGH'; - else if (hasPreference) persistence = 'MEDIUM'; - - // Detect parameters (ports, paths, etc.) - const parameters = this._extractParameters(statement); - - // Analyze quality using RuleOptimizer - const analysis = RuleOptimizer.analyzeRule(statement); - - // Determine quality tier - let quality = 'TOO_NEBULOUS'; - let autoConvert = false; - - if (analysis.overallScore >= 80) { - quality = 'HIGH'; - autoConvert = true; - } else if (analysis.overallScore >= 60) { - quality = 'NEEDS_CLARIFICATION'; - autoConvert = false; - } - - // Generate optimized version - const optimized = RuleOptimizer.optimize(statement, { mode: 'aggressive' }); - - candidates.push({ - originalText: statement, - sectionTitle: section.title, - quadrant, - persistence, - parameters, - quality, - autoConvert, - analysis: { - clarityScore: analysis.clarity.score, - specificityScore: analysis.specificity.score, - actionabilityScore: analysis.actionability.score, - overallScore: analysis.overallScore, - issues: [ - ...analysis.clarity.issues, - ...analysis.specificity.issues, - ...analysis.actionability.issues - ] - }, - suggestedRule: { - text: optimized.optimized, - scope: this._determineScope(optimized.optimized), - quadrant, - persistence, - variables: this._detectVariables(optimized.optimized), - clarityScore: RuleOptimizer.analyzeRule(optimized.optimized).overallScore - }, - improvements: analysis.suggestions.map(s => s.reason) - }); - }); - }); - - return candidates; - } - - /** - * Detect redundant rules - * - * @param {Array} candidates - Candidate rules - * @returns {Array} Redundancy groups with merge suggestions - */ - detectRedundancies(candidates) { - const redundancies = []; - const processed = new Set(); - - for (let i = 0; i < candidates.length; i++) { - if (processed.has(i)) continue; - - const similar = []; - for (let j = i + 1; j < candidates.length; j++) { - if (processed.has(j)) continue; - - const similarity = this._calculateSimilarity( - candidates[i].originalText, - candidates[j].originalText - ); - - if (similarity > 0.7) { - similar.push(j); - } - } - - if (similar.length > 0) { - const group = [candidates[i], ...similar.map(idx => candidates[idx])]; - similar.forEach(idx => processed.add(idx)); - processed.add(i); - - redundancies.push({ - rules: group.map(c => c.originalText), - mergeSuggestion: this._suggestMerge(group) - }); - } - } - - return redundancies; - } - - /** - * Generate migration plan from analysis - * - * @param {Object} analysis - Complete analysis with candidates and redundancies - * @returns {Object} Migration plan - */ - generateMigrationPlan(analysis) { - const { candidates, redundancies } = analysis; - - const highQuality = candidates.filter(c => c.quality === 'HIGH'); - const needsClarification = candidates.filter(c => c.quality === 'NEEDS_CLARIFICATION'); - const tooNebulous = candidates.filter(c => c.quality === 'TOO_NEBULOUS'); - - return { - summary: { - totalStatements: candidates.length, - highQuality: highQuality.length, - needsClarification: needsClarification.length, - tooNebulous: tooNebulous.length, - redundancies: redundancies.length, - autoConvertable: candidates.filter(c => c.autoConvert).length - }, - steps: [ - { - phase: 'Auto-Convert', - count: highQuality.length, - description: 'High-quality rules that can be auto-converted', - rules: highQuality.map(c => c.suggestedRule) - }, - { - phase: 'Review & Clarify', - count: needsClarification.length, - description: 'Rules needing clarification before conversion', - rules: needsClarification.map(c => ({ - original: c.originalText, - suggested: c.suggestedRule, - issues: c.analysis.issues, - improvements: c.improvements - })) - }, - { - phase: 'Manual Rewrite', - count: tooNebulous.length, - description: 'Statements too vague - require manual rewrite', - rules: tooNebulous.map(c => ({ - original: c.originalText, - suggestions: c.improvements - })) - }, - { - phase: 'Merge Redundancies', - count: redundancies.length, - description: 'Similar rules that should be merged', - groups: redundancies - } - ], - estimatedTime: this._estimateMigrationTime(candidates, redundancies) - }; - } - - /** - * Analyze complete CLAUDE.md file - * - * @param {string} content - CLAUDE.md content - * @returns {Object} Complete analysis - */ - analyze(content) { - const parsed = this.parse(content); - const candidates = this.extractCandidateRules(parsed); - const redundancies = this.detectRedundancies(candidates); - const migrationPlan = this.generateMigrationPlan({ candidates, redundancies }); - - return { - parsed, - candidates, - redundancies, - migrationPlan, - quality: { - highQuality: candidates.filter(c => c.quality === 'HIGH').length, - needsClarification: candidates.filter(c => c.quality === 'NEEDS_CLARIFICATION').length, - tooNebulous: candidates.filter(c => c.quality === 'TOO_NEBULOUS').length, - averageScore: Math.round( - candidates.reduce((sum, c) => sum + c.analysis.overallScore, 0) / candidates.length - ) - } - }; - } - - // ========== PRIVATE HELPER METHODS ========== - - /** - * Classify statement into Tractatus quadrant - * @private - */ - _classifyQuadrant(statement) { - const lower = statement.toLowerCase(); - let bestMatch = 'TACTICAL'; - let maxMatches = 0; - - for (const [quadrant, keywords] of Object.entries(this.quadrantKeywords)) { - const matches = keywords.filter(keyword => lower.includes(keyword)).length; - if (matches > maxMatches) { - maxMatches = matches; - bestMatch = quadrant; - } - } - - return bestMatch; - } - - /** - * Extract parameters from statement (ports, paths, etc.) - * @private - */ - _extractParameters(statement) { - const parameters = {}; - - // Port numbers - const portMatch = statement.match(/port\s+(\d+)/i); - if (portMatch) { - parameters.port = portMatch[1]; - } - - // Database types - const dbMatch = statement.match(/\b(mongodb|postgresql|mysql|redis)\b/i); - if (dbMatch) { - parameters.database = dbMatch[1]; - } - - // Paths - const pathMatch = statement.match(/[\/\\][\w\/\\.-]+/); - if (pathMatch) { - parameters.path = pathMatch[0]; - } - - // Environment - const envMatch = statement.match(/\b(production|development|staging|test)\b/i); - if (envMatch) { - parameters.environment = envMatch[1]; - } - - return parameters; - } - - /** - * Detect variables in optimized text - * @private - */ - _detectVariables(text) { - const matches = text.matchAll(/\$\{([A-Z_]+)\}/g); - return Array.from(matches, m => m[1]); - } - - /** - * Determine if rule should be universal or project-specific - * @private - */ - _determineScope(text) { - // If has variables, likely universal - if (this._detectVariables(text).length > 0) { - return 'UNIVERSAL'; - } - - // If references specific project name, project-specific - if (/\b(tractatus|family-history|sydigital)\b/i.test(text)) { - return 'PROJECT_SPECIFIC'; - } - - // Default to universal for reusability - return 'UNIVERSAL'; - } - - /** - * Calculate text similarity (Jaccard coefficient) - * @private - */ - _calculateSimilarity(text1, text2) { - const words1 = new Set(text1.toLowerCase().split(/\s+/)); - const words2 = new Set(text2.toLowerCase().split(/\s+/)); - - const intersection = new Set([...words1].filter(w => words2.has(w))); - const union = new Set([...words1, ...words2]); - - return intersection.size / union.size; - } - - /** - * Suggest merged rule from similar rules - * @private - */ - _suggestMerge(group) { - // Take the most specific rule as base - const sorted = group.sort((a, b) => - b.analysis.specificityScore - a.analysis.specificityScore - ); - - return sorted[0].suggestedRule.text; - } - - /** - * Estimate time needed for migration - * @private - */ - _estimateMigrationTime(candidates, redundancies) { - const autoConvert = candidates.filter(c => c.autoConvert).length; - const needsReview = candidates.filter(c => !c.autoConvert && c.quality !== 'TOO_NEBULOUS').length; - const needsRewrite = candidates.filter(c => c.quality === 'TOO_NEBULOUS').length; - - // Auto-convert: 1 min each (review) - // Needs review: 5 min each (review + edit) - // Needs rewrite: 10 min each (rewrite from scratch) - // Redundancies: 3 min each (merge) - - const minutes = (autoConvert * 1) + - (needsReview * 5) + - (needsRewrite * 10) + - (redundancies.length * 3); - - return { - minutes, - hours: Math.round(minutes / 60 * 10) / 10, - breakdown: { - autoConvert: `${autoConvert} rules × 1 min = ${autoConvert} min`, - needsReview: `${needsReview} rules × 5 min = ${needsReview * 5} min`, - needsRewrite: `${needsRewrite} rules × 10 min = ${needsRewrite * 10} min`, - redundancies: `${redundancies.length} groups × 3 min = ${redundancies.length * 3} min` - } - }; - } -} - -module.exports = new ClaudeMdAnalyzer(); diff --git a/src/services/MediaTriage.service.js b/src/services/MediaTriage.service.js deleted file mode 100644 index c767ff84..00000000 --- a/src/services/MediaTriage.service.js +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Media Triage Service - * AI-powered media inquiry triage with Tractatus governance - * - * GOVERNANCE PRINCIPLES: - * - AI analyzes and suggests, humans decide - * - All reasoning must be transparent - * - Values decisions require human approval - * - No auto-responses without human review - * - Boundary enforcement for sensitive topics - */ - -const Anthropic = require('@anthropic-ai/sdk'); -const logger = require('../utils/logger.util'); - -class MediaTriageService { - constructor() { - // Initialize Anthropic client - this.client = new Anthropic({ - apiKey: process.env.CLAUDE_API_KEY - }); - - // Topic sensitivity keywords (triggers boundary enforcement) - this.SENSITIVE_TOPICS = [ - 'values', 'ethics', 'strategic direction', 'partnerships', - 'te tiriti', 'māori', 'indigenous', 'governance philosophy', - 'framework limitations', 'criticism', 'controversy' - ]; - - // Urgency indicators - this.URGENCY_INDICATORS = { - high: ['urgent', 'asap', 'immediate', 'breaking', 'deadline today', 'deadline tomorrow'], - medium: ['deadline this week', 'timely', 'soon'], - low: ['no deadline', 'general inquiry', 'background'] - }; - } - - /** - * Perform AI triage on media inquiry - * Returns structured analysis for human review - */ - async triageInquiry(inquiry) { - try { - logger.info(`AI triaging inquiry: ${inquiry._id}`); - - // Step 1: Analyze urgency - const urgencyAnalysis = await this.analyzeUrgency(inquiry); - - // Step 2: Detect topic sensitivity - const sensitivityAnalysis = await this.analyzeTopicSensitivity(inquiry); - - // Step 3: Check if involves values (BoundaryEnforcer) - const valuesCheck = this.checkInvolvesValues(inquiry, sensitivityAnalysis); - - // Step 4: Generate suggested talking points - const talkingPoints = await this.generateTalkingPoints(inquiry, sensitivityAnalysis); - - // Step 5: Draft response (ALWAYS requires human approval) - const draftResponse = await this.generateDraftResponse(inquiry, talkingPoints, valuesCheck); - - // Step 6: Calculate suggested response time - const suggestedResponseTime = this.calculateResponseTime(urgencyAnalysis, inquiry); - - // Compile triage result with full transparency - const triageResult = { - urgency: urgencyAnalysis.level, - urgency_score: urgencyAnalysis.score, - urgency_reasoning: urgencyAnalysis.reasoning, - - topic_sensitivity: sensitivityAnalysis.level, - sensitivity_reasoning: sensitivityAnalysis.reasoning, - - involves_values: valuesCheck.involves_values, - values_reasoning: valuesCheck.reasoning, - boundary_enforcement: valuesCheck.boundary_enforcement, - - suggested_response_time: suggestedResponseTime, - suggested_talking_points: talkingPoints, - - draft_response: draftResponse.content, - draft_response_reasoning: draftResponse.reasoning, - draft_requires_human_approval: true, // ALWAYS - - triaged_at: new Date(), - ai_model: 'claude-3-5-sonnet-20241022', - framework_compliance: { - boundary_enforcer_checked: true, - human_approval_required: true, - reasoning_transparent: true - } - }; - - logger.info(`Triage complete for inquiry ${inquiry._id}: urgency=${urgencyAnalysis.level}, values=${valuesCheck.involves_values}`); - - return triageResult; - - } catch (error) { - logger.error('Media triage error:', error); - throw new Error(`Triage failed: ${error.message}`); - } - } - - /** - * Analyze urgency level of inquiry - */ - async analyzeUrgency(inquiry) { - const prompt = `Analyze the urgency of this media inquiry and provide a structured assessment. - -INQUIRY DETAILS: -Subject: ${inquiry.inquiry.subject} -Message: ${inquiry.inquiry.message} -Deadline: ${inquiry.inquiry.deadline || 'Not specified'} -Outlet: ${inquiry.contact.outlet} - -TASK: -1. Determine urgency level: HIGH, MEDIUM, or LOW -2. Provide urgency score (0-100) -3. Explain your reasoning - -URGENCY GUIDELINES: -- HIGH (80-100): Breaking news, same-day deadline, crisis response -- MEDIUM (40-79): This week deadline, feature story, ongoing coverage -- LOW (0-39): No deadline, background research, general inquiry - -Respond in JSON format: -{ - "level": "HIGH|MEDIUM|LOW", - "score": 0-100, - "reasoning": "2-3 sentence explanation" -}`; - - try { - const message = await this.client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 500, - messages: [{ - role: 'user', - content: prompt - }] - }); - - const responseText = message.content[0].text; - const analysis = JSON.parse(responseText); - - return { - level: analysis.level.toLowerCase(), - score: analysis.score, - reasoning: analysis.reasoning - }; - - } catch (error) { - logger.error('Urgency analysis error:', error); - // Fallback to basic analysis - return this.basicUrgencyAnalysis(inquiry); - } - } - - /** - * Analyze topic sensitivity - */ - async analyzeTopicSensitivity(inquiry) { - const prompt = `Analyze the topic sensitivity of this media inquiry for an AI safety framework organization. - -INQUIRY DETAILS: -Subject: ${inquiry.inquiry.subject} -Message: ${inquiry.inquiry.message} -Topics: ${inquiry.inquiry.topic_areas?.join(', ') || 'Not specified'} - -TASK: -Determine if this inquiry touches on sensitive topics such as: -- Framework values or ethics -- Strategic partnerships -- Indigenous data sovereignty (Te Tiriti o Waitangi) -- Framework limitations or criticisms -- Controversial AI safety debates - -Provide sensitivity level: HIGH, MEDIUM, or LOW - -Respond in JSON format: -{ - "level": "HIGH|MEDIUM|LOW", - "reasoning": "2-3 sentence explanation of why this topic is sensitive or not" -}`; - - try { - const message = await this.client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 500, - messages: [{ - role: 'user', - content: prompt - }] - }); - - const responseText = message.content[0].text; - const analysis = JSON.parse(responseText); - - return { - level: analysis.level.toLowerCase(), - reasoning: analysis.reasoning - }; - - } catch (error) { - logger.error('Sensitivity analysis error:', error); - // Fallback to keyword-based analysis - return this.basicSensitivityAnalysis(inquiry); - } - } - - /** - * Check if inquiry involves framework values (BoundaryEnforcer) - */ - checkInvolvesValues(inquiry, sensitivityAnalysis) { - // Keywords that indicate values territory - const valuesKeywords = [ - 'values', 'ethics', 'mission', 'principles', 'philosophy', - 'te tiriti', 'indigenous', 'sovereignty', 'partnership', - 'governance', 'strategy', 'direction', 'why tractatus' - ]; - - const combinedText = `${inquiry.inquiry.subject} ${inquiry.inquiry.message}`.toLowerCase(); - const hasValuesKeyword = valuesKeywords.some(keyword => combinedText.includes(keyword)); - const isHighSensitivity = sensitivityAnalysis.level === 'high'; - - const involves_values = hasValuesKeyword || isHighSensitivity; - - return { - involves_values, - reasoning: involves_values - ? 'This inquiry touches on framework values, strategic direction, or sensitive topics. Human approval required for any response (BoundaryEnforcer).' - : 'This inquiry is operational/technical in nature. Standard response workflow applies.', - boundary_enforcement: involves_values - ? 'ENFORCED: Response must be reviewed and approved by John Stroh before sending.' - : 'NOT_REQUIRED: Standard review process applies.', - escalation_required: involves_values, - escalation_reason: involves_values - ? 'Values-sensitive topic detected by BoundaryEnforcer' - : null - }; - } - - /** - * Generate suggested talking points - */ - async generateTalkingPoints(inquiry, sensitivityAnalysis) { - const prompt = `Generate 3-5 concise talking points for responding to this media inquiry about an AI safety framework. - -INQUIRY DETAILS: -Subject: ${inquiry.inquiry.subject} -Message: ${inquiry.inquiry.message} -Sensitivity: ${sensitivityAnalysis.level} - -GUIDELINES: -- Focus on factual, verifiable information -- Avoid speculation or aspirational claims -- Stay within established framework documentation -- Be honest about limitations -- NO fabricated statistics -- NO absolute guarantees - -Respond with JSON array of talking points: -["Point 1", "Point 2", "Point 3", ...]`; - - try { - const message = await this.client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 800, - messages: [{ - role: 'user', - content: prompt - }] - }); - - const responseText = message.content[0].text; - const points = JSON.parse(responseText); - - return Array.isArray(points) ? points : []; - - } catch (error) { - logger.error('Talking points generation error:', error); - return [ - 'Tractatus is a development-stage AI safety framework', - 'Focus on architectural safety guarantees and human oversight', - 'Open source and transparent governance' - ]; - } - } - - /** - * Generate draft response (ALWAYS requires human approval) - */ - async generateDraftResponse(inquiry, talkingPoints, valuesCheck) { - const prompt = `Draft a professional response to this media inquiry. This draft will be reviewed and edited by humans before sending. - -INQUIRY DETAILS: -From: ${inquiry.contact.name} (${inquiry.contact.outlet}) -Subject: ${inquiry.inquiry.subject} -Message: ${inquiry.inquiry.message} - -TALKING POINTS TO INCLUDE: -${talkingPoints.map((p, i) => `${i + 1}. ${p}`).join('\n')} - -VALUES CHECK: -${valuesCheck.involves_values ? '⚠️ This touches on framework values - response requires strategic approval' : 'Standard operational inquiry'} - -GUIDELINES: -- Professional and helpful tone -- 2-3 paragraphs maximum -- Include contact info for follow-up -- Offer to provide additional resources -- Be honest about framework status (development stage) -- NO fabricated statistics or guarantees - -Draft the response:`; - - try { - const message = await this.client.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 1000, - messages: [{ - role: 'user', - content: prompt - }] - }); - - const draftContent = message.content[0].text; - - return { - content: draftContent, - reasoning: 'AI-generated draft based on talking points. MUST be reviewed and approved by human before sending.', - requires_approval: true, - approval_level: valuesCheck.involves_values ? 'STRATEGIC' : 'OPERATIONAL' - }; - - } catch (error) { - logger.error('Draft response generation error:', error); - return { - content: `[DRAFT GENERATION FAILED - Manual response required]\n\nHi ${inquiry.contact.name},\n\nThank you for your inquiry about Tractatus. We'll get back to you shortly with a detailed response.\n\nBest regards,\nTractatus Team`, - reasoning: 'Fallback template due to AI generation error', - requires_approval: true, - approval_level: 'OPERATIONAL' - }; - } - } - - /** - * Calculate suggested response time in hours - */ - calculateResponseTime(urgencyAnalysis, inquiry) { - if (inquiry.inquiry.deadline) { - const deadline = new Date(inquiry.inquiry.deadline); - const now = new Date(); - const hoursUntilDeadline = (deadline - now) / (1000 * 60 * 60); - return Math.max(1, Math.floor(hoursUntilDeadline * 0.5)); // Aim for 50% of time to deadline - } - - // Based on urgency score - if (urgencyAnalysis.level === 'high') { - return 4; // 4 hours - } else if (urgencyAnalysis.level === 'medium') { - return 24; // 1 day - } else { - return 72; // 3 days - } - } - - /** - * Basic urgency analysis (fallback) - */ - basicUrgencyAnalysis(inquiry) { - const text = `${inquiry.inquiry.subject} ${inquiry.inquiry.message}`.toLowerCase(); - let score = 30; // Default low - let level = 'low'; - - // Check for urgency keywords - for (const [urgencyLevel, keywords] of Object.entries(this.URGENCY_INDICATORS)) { - for (const keyword of keywords) { - if (text.includes(keyword)) { - if (urgencyLevel === 'high') { - score = 85; - level = 'high'; - } else if (urgencyLevel === 'medium' && score < 60) { - score = 60; - level = 'medium'; - } - } - } - } - - // Check deadline - if (inquiry.inquiry.deadline) { - const deadline = new Date(inquiry.inquiry.deadline); - const now = new Date(); - const hoursUntilDeadline = (deadline - now) / (1000 * 60 * 60); - - if (hoursUntilDeadline < 24) { - score = 90; - level = 'high'; - } else if (hoursUntilDeadline < 72) { - score = 65; - level = 'medium'; - } - } - - return { - level, - score, - reasoning: `Basic analysis based on keywords and deadline. Urgency level: ${level}.` - }; - } - - /** - * Basic sensitivity analysis (fallback) - */ - basicSensitivityAnalysis(inquiry) { - const text = `${inquiry.inquiry.subject} ${inquiry.inquiry.message}`.toLowerCase(); - let level = 'low'; - - for (const keyword of this.SENSITIVE_TOPICS) { - if (text.includes(keyword)) { - level = 'high'; - break; - } - } - - return { - level, - reasoning: level === 'high' - ? 'Topic involves potentially sensitive framework areas' - : 'Standard operational inquiry' - }; - } - - /** - * Get triage statistics for transparency - */ - async getTriageStats(inquiries) { - const stats = { - total_triaged: inquiries.length, - by_urgency: { - high: 0, - medium: 0, - low: 0 - }, - by_sensitivity: { - high: 0, - medium: 0, - low: 0 - }, - involves_values_count: 0, - boundary_enforcements: 0, - avg_response_time_hours: 0, - human_overrides: 0 - }; - - for (const inquiry of inquiries) { - if (inquiry.ai_triage) { - // Count by urgency - if (inquiry.ai_triage.urgency) { - stats.by_urgency[inquiry.ai_triage.urgency]++; - } - - // Count by sensitivity - if (inquiry.ai_triage.topic_sensitivity) { - stats.by_sensitivity[inquiry.ai_triage.topic_sensitivity]++; - } - - // Count values involvements - if (inquiry.ai_triage.involves_values) { - stats.involves_values_count++; - stats.boundary_enforcements++; - } - - // Average response time - if (inquiry.ai_triage.suggested_response_time) { - stats.avg_response_time_hours += inquiry.ai_triage.suggested_response_time; - } - } - } - - if (stats.total_triaged > 0) { - stats.avg_response_time_hours = Math.round(stats.avg_response_time_hours / stats.total_triaged); - } - - return stats; - } -} - -module.exports = new MediaTriageService(); diff --git a/src/services/koha.service.js b/src/services/koha.service.js deleted file mode 100644 index 3fd2a0b2..00000000 --- a/src/services/koha.service.js +++ /dev/null @@ -1,515 +0,0 @@ -/** - * Koha Service - * Donation processing service for Tractatus Framework - * - * Based on passport-consolidated's StripeService pattern - * Handles multi-currency donations via Stripe (reusing existing account) - */ - -const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); -const Donation = require('../models/Donation.model'); -const { - isSupportedCurrency, - convertToNZD, - getExchangeRate -} = require('../config/currencies.config'); - -// Simple logger (uses console) -const logger = { - info: (...args) => console.log(...args), - error: (...args) => console.error(...args), - warn: (...args) => console.warn(...args) -}; - -class KohaService { - constructor() { - this.stripe = stripe; - this.priceIds = { - // NZD monthly tiers - monthly_5: process.env.STRIPE_KOHA_5_PRICE_ID, - monthly_15: process.env.STRIPE_KOHA_15_PRICE_ID, - monthly_50: process.env.STRIPE_KOHA_50_PRICE_ID, - // One-time donation (custom amount) - one_time: process.env.STRIPE_KOHA_ONETIME_PRICE_ID - }; - } - - /** - * Create a Stripe Checkout session for donation - * @param {Object} donationData - Donation details - * @returns {Object} Checkout session data - */ - async createCheckoutSession(donationData) { - try { - const { amount, currency, frequency, tier, donor, public_acknowledgement, public_name } = donationData; - - // Validate currency - const donationCurrency = (currency || 'nzd').toUpperCase(); - if (!isSupportedCurrency(donationCurrency)) { - throw new Error(`Unsupported currency: ${donationCurrency}`); - } - - // Validate inputs - if (!amount || amount < 100) { - throw new Error('Minimum donation amount is $1.00'); - } - - if (!frequency || !['monthly', 'one_time'].includes(frequency)) { - throw new Error('Invalid frequency. Must be "monthly" or "one_time"'); - } - - if (!donor?.email) { - throw new Error('Donor email is required for receipt'); - } - - // Calculate NZD equivalent for transparency metrics - const amountNZD = donationCurrency === 'NZD' ? amount : convertToNZD(amount, donationCurrency); - const exchangeRate = getExchangeRate(donationCurrency); - - logger.info(`[KOHA] Creating checkout session: ${frequency} donation of ${donationCurrency} $${amount / 100} (NZD $${amountNZD / 100})`); - - // Create or retrieve Stripe customer - let stripeCustomer; - try { - // Search for existing customer by email - const customers = await this.stripe.customers.list({ - email: donor.email, - limit: 1 - }); - - if (customers.data.length > 0) { - stripeCustomer = customers.data[0]; - logger.info(`[KOHA] Using existing customer ${stripeCustomer.id}`); - } else { - stripeCustomer = await this.stripe.customers.create({ - email: donor.email, - name: donor.name || 'Anonymous Donor', - metadata: { - source: 'tractatus_koha', - public_acknowledgement: public_acknowledgement ? 'yes' : 'no' - } - }); - logger.info(`[KOHA] Created new customer ${stripeCustomer.id}`); - } - } catch (error) { - logger.error('[KOHA] Failed to create/retrieve customer:', error); - throw new Error('Failed to process donor information'); - } - - // Prepare checkout session parameters - const sessionParams = { - payment_method_types: ['card'], - customer: stripeCustomer.id, - mode: frequency === 'monthly' ? 'subscription' : 'payment', - success_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha/success.html?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${process.env.FRONTEND_URL || 'https://agenticgovernance.digital'}/koha.html`, - metadata: { - frequency: frequency, - tier: tier, - currency: donationCurrency, - amount_nzd: String(amountNZD), - exchange_rate: String(exchangeRate), - donor_name: donor.name || 'Anonymous', - public_acknowledgement: public_acknowledgement ? 'yes' : 'no', - public_name: public_name || '', - source: 'tractatus_website' - }, - allow_promotion_codes: true, // Allow coupon codes - billing_address_collection: 'auto' - }; - - // Add line items based on frequency - if (frequency === 'monthly') { - // Subscription mode - use price ID for recurring donations - const priceId = this.priceIds[`monthly_${tier}`]; - if (!priceId) { - throw new Error(`Invalid monthly tier: ${tier}`); - } - - sessionParams.line_items = [{ - price: priceId, - quantity: 1 - }]; - - sessionParams.subscription_data = { - metadata: { - tier: tier, - public_acknowledgement: public_acknowledgement ? 'yes' : 'no' - } - }; - } else { - // One-time payment mode - use custom amount - sessionParams.line_items = [{ - price_data: { - currency: donationCurrency.toLowerCase(), - product_data: { - name: 'Tractatus Framework Support', - description: 'One-time donation to support the Tractatus Framework for AI safety', - images: ['https://agenticgovernance.digital/images/tractatus-icon.svg'] - }, - unit_amount: amount // Amount in cents - }, - quantity: 1 - }]; - - sessionParams.payment_intent_data = { - metadata: { - tier: tier || 'custom', - public_acknowledgement: public_acknowledgement ? 'yes' : 'no' - } - }; - } - - // Create checkout session - const session = await this.stripe.checkout.sessions.create(sessionParams); - - logger.info(`[KOHA] Checkout session created: ${session.id}`); - - // Create pending donation record in database - await Donation.create({ - amount: amount, - currency: donationCurrency.toLowerCase(), - amount_nzd: amountNZD, - exchange_rate_to_nzd: exchangeRate, - frequency: frequency, - tier: tier, - donor: { - name: donor.name || 'Anonymous', - email: donor.email, - country: donor.country - }, - public_acknowledgement: public_acknowledgement || false, - public_name: public_name || null, - stripe: { - customer_id: stripeCustomer.id - }, - status: 'pending', - metadata: { - source: 'website', - session_id: session.id - } - }); - - return { - sessionId: session.id, - checkoutUrl: session.url, - frequency: frequency, - amount: amount / 100 - }; - - } catch (error) { - logger.error('[KOHA] Checkout session creation failed:', error); - throw error; - } - } - - /** - * Handle webhook events from Stripe - * @param {Object} event - Stripe webhook event - */ - async handleWebhook(event) { - try { - logger.info(`[KOHA] Processing webhook event: ${event.type}`); - - switch (event.type) { - case 'checkout.session.completed': - await this.handleCheckoutComplete(event.data.object); - break; - - case 'payment_intent.succeeded': - await this.handlePaymentSuccess(event.data.object); - break; - - case 'payment_intent.payment_failed': - await this.handlePaymentFailure(event.data.object); - break; - - case 'invoice.paid': - // Recurring subscription payment succeeded - await this.handleInvoicePaid(event.data.object); - break; - - case 'invoice.payment_failed': - // Recurring subscription payment failed - await this.handleInvoicePaymentFailed(event.data.object); - break; - - case 'customer.subscription.created': - case 'customer.subscription.updated': - await this.handleSubscriptionUpdate(event.data.object); - break; - - case 'customer.subscription.deleted': - await this.handleSubscriptionCancellation(event.data.object); - break; - - default: - logger.info(`[KOHA] Unhandled webhook event type: ${event.type}`); - } - - } catch (error) { - logger.error('[KOHA] Webhook processing error:', error); - throw error; - } - } - - /** - * Handle successful checkout completion - */ - async handleCheckoutComplete(session) { - try { - const frequency = session.metadata.frequency; - const tier = session.metadata.tier; - const currency = session.metadata.currency || session.currency?.toUpperCase() || 'NZD'; - const amountNZD = session.metadata.amount_nzd ? parseInt(session.metadata.amount_nzd) : session.amount_total; - const exchangeRate = session.metadata.exchange_rate ? parseFloat(session.metadata.exchange_rate) : 1.0; - - logger.info(`[KOHA] Checkout completed: ${frequency} donation, tier: ${tier}, currency: ${currency}`); - - // Find pending donation or create new one - let donation = await Donation.findByPaymentIntentId(session.payment_intent); - - if (!donation) { - // Create donation record from session data - donation = await Donation.create({ - amount: session.amount_total, - currency: currency.toLowerCase(), - amount_nzd: amountNZD, - exchange_rate_to_nzd: exchangeRate, - frequency: frequency, - tier: tier, - donor: { - name: session.metadata.donor_name || 'Anonymous', - email: session.customer_email - }, - public_acknowledgement: session.metadata.public_acknowledgement === 'yes', - public_name: session.metadata.public_name || null, - stripe: { - customer_id: session.customer, - subscription_id: session.subscription || null, - payment_intent_id: session.payment_intent - }, - status: 'completed', - payment_date: new Date() - }); - } else { - // Update existing donation - await Donation.updateStatus(donation._id, 'completed', { - 'stripe.subscription_id': session.subscription || null, - 'stripe.payment_intent_id': session.payment_intent, - payment_date: new Date() - }); - } - - // Send receipt email (async, don't wait) - this.sendReceiptEmail(donation).catch(err => - logger.error('[KOHA] Failed to send receipt email:', err) - ); - - logger.info(`[KOHA] Donation recorded: ${currency} $${session.amount_total / 100} (NZD $${amountNZD / 100})`); - - } catch (error) { - logger.error('[KOHA] Error handling checkout completion:', error); - throw error; - } - } - - /** - * Handle successful payment - */ - async handlePaymentSuccess(paymentIntent) { - try { - logger.info(`[KOHA] Payment succeeded: ${paymentIntent.id}`); - - const donation = await Donation.findByPaymentIntentId(paymentIntent.id); - if (donation && donation.status === 'pending') { - await Donation.updateStatus(donation._id, 'completed', { - payment_date: new Date() - }); - } - - } catch (error) { - logger.error('[KOHA] Error handling payment success:', error); - } - } - - /** - * Handle failed payment - */ - async handlePaymentFailure(paymentIntent) { - try { - logger.warn(`[KOHA] Payment failed: ${paymentIntent.id}`); - - const donation = await Donation.findByPaymentIntentId(paymentIntent.id); - if (donation) { - await Donation.updateStatus(donation._id, 'failed', { - 'metadata.failure_reason': paymentIntent.last_payment_error?.message - }); - } - - } catch (error) { - logger.error('[KOHA] Error handling payment failure:', error); - } - } - - /** - * Handle paid invoice (recurring subscription payment) - */ - async handleInvoicePaid(invoice) { - try { - logger.info(`[KOHA] Invoice paid: ${invoice.id} for subscription ${invoice.subscription}`); - - // Create new donation record for this recurring payment - const subscription = await this.stripe.subscriptions.retrieve(invoice.subscription); - - // Get currency from invoice or metadata - const currency = (invoice.currency || subscription.metadata.currency || 'NZD').toUpperCase(); - const amount = invoice.amount_paid; - - // Calculate NZD equivalent - const amountNZD = currency === 'NZD' ? amount : convertToNZD(amount, currency); - const exchangeRate = getExchangeRate(currency); - - await Donation.create({ - amount: amount, - currency: currency.toLowerCase(), - amount_nzd: amountNZD, - exchange_rate_to_nzd: exchangeRate, - frequency: 'monthly', - tier: subscription.metadata.tier, - donor: { - email: invoice.customer_email - }, - public_acknowledgement: subscription.metadata.public_acknowledgement === 'yes', - stripe: { - customer_id: invoice.customer, - subscription_id: invoice.subscription, - invoice_id: invoice.id, - charge_id: invoice.charge - }, - status: 'completed', - payment_date: new Date(invoice.created * 1000) - }); - - logger.info(`[KOHA] Recurring donation recorded: ${currency} $${amount / 100} (NZD $${amountNZD / 100})`); - - } catch (error) { - logger.error('[KOHA] Error handling invoice paid:', error); - } - } - - /** - * Handle failed invoice payment - */ - async handleInvoicePaymentFailed(invoice) { - try { - logger.warn(`[KOHA] Invoice payment failed: ${invoice.id}`); - // Could send notification email to donor here - - } catch (error) { - logger.error('[KOHA] Error handling invoice payment failed:', error); - } - } - - /** - * Handle subscription updates - */ - async handleSubscriptionUpdate(subscription) { - logger.info(`[KOHA] Subscription updated: ${subscription.id}, status: ${subscription.status}`); - } - - /** - * Handle subscription cancellation - */ - async handleSubscriptionCancellation(subscription) { - try { - logger.info(`[KOHA] Subscription cancelled: ${subscription.id}`); - - await Donation.cancelSubscription(subscription.id); - - } catch (error) { - logger.error('[KOHA] Error handling subscription cancellation:', error); - } - } - - /** - * Verify webhook signature - */ - verifyWebhookSignature(payload, signature) { - try { - return this.stripe.webhooks.constructEvent( - payload, - signature, - process.env.STRIPE_KOHA_WEBHOOK_SECRET - ); - } catch (error) { - logger.error('[KOHA] Webhook signature verification failed:', error); - throw new Error('Invalid webhook signature'); - } - } - - /** - * Get transparency metrics for public dashboard - */ - async getTransparencyMetrics() { - try { - return await Donation.getTransparencyMetrics(); - } catch (error) { - logger.error('[KOHA] Error getting transparency metrics:', error); - throw error; - } - } - - /** - * Send receipt email (placeholder) - */ - async sendReceiptEmail(donation) { - // TODO: Implement email service integration - logger.info(`[KOHA] Receipt email would be sent to ${donation.donor.email}`); - - // Generate receipt number - const receiptNumber = `KOHA-${new Date().getFullYear()}-${String(donation._id).slice(-8).toUpperCase()}`; - - await Donation.markReceiptSent(donation._id, receiptNumber); - - return true; - } - - /** - * Cancel a recurring donation (admin or donor-initiated) - */ - async cancelRecurringDonation(subscriptionId) { - try { - logger.info(`[KOHA] Cancelling subscription: ${subscriptionId}`); - - // Cancel in Stripe - await this.stripe.subscriptions.cancel(subscriptionId); - - // Update database - await Donation.cancelSubscription(subscriptionId); - - return { success: true, message: 'Subscription cancelled successfully' }; - - } catch (error) { - logger.error('[KOHA] Error cancelling subscription:', error); - throw error; - } - } - - /** - * Get donation statistics (admin only) - */ - async getStatistics(startDate = null, endDate = null) { - try { - return await Donation.getStatistics(startDate, endDate); - } catch (error) { - logger.error('[KOHA] Error getting statistics:', error); - throw error; - } - } -} - -// Create singleton instance -const kohaService = new KohaService(); - -module.exports = kohaService; diff --git a/src/utils/document-section-parser.js b/src/utils/document-section-parser.js deleted file mode 100644 index 08e0fd65..00000000 --- a/src/utils/document-section-parser.js +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Document Section Parser - * Analyzes markdown documents and creates card-based sections - */ - -/** - * Parse document into sections based on H2 headings - */ -function parseDocumentSections(markdown, contentHtml) { - if (!markdown) return []; - - const sections = []; - const lines = markdown.split('\n'); - let currentSection = null; - let sectionContent = []; - - // Find H1 (document title) first - let documentTitle = ''; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const h1Match = line.match(/^#\s+(.+)$/); - if (h1Match) { - documentTitle = h1Match[1].trim(); - break; - } - } - - // Parse sections by H2 headings - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - // Check for H2 heading (## Heading) - const h2Match = line.match(/^##\s+(.+)$/); - - if (h2Match) { - // Save previous section if exists - if (currentSection) { - currentSection.content = sectionContent.join('\n').trim(); - currentSection.excerpt = extractExcerpt(currentSection.content); - currentSection.readingTime = estimateReadingTime(currentSection.content); - currentSection.technicalLevel = detectTechnicalLevel(currentSection.content); - currentSection.category = categorizeSection(currentSection.title, currentSection.content); - sections.push(currentSection); - } - - // Start new section - const title = h2Match[1].trim(); - const slug = generateSlug(title); - - currentSection = { - title, - slug, - level: 2, - content: '', - excerpt: '', - readingTime: 0, - technicalLevel: 'basic', - category: 'conceptual' - }; - - // Include the H2 heading itself in the section content - sectionContent = [line]; - } else if (currentSection) { - // Only add content until we hit another H2 or H1 - const isH1 = line.match(/^#\s+[^#]/); - - if (isH1) { - // Skip H1 (document title) - don't add to section - continue; - } - - // Add all other content (including H3, H4, paragraphs, etc.) - sectionContent.push(line); - } - } - - // Save last section - if (currentSection && sectionContent.length > 0) { - currentSection.content = sectionContent.join('\n').trim(); - currentSection.excerpt = extractExcerpt(currentSection.content); - currentSection.readingTime = estimateReadingTime(currentSection.content); - currentSection.technicalLevel = detectTechnicalLevel(currentSection.content); - currentSection.category = categorizeSection(currentSection.title, currentSection.content); - sections.push(currentSection); - } - - return sections; -} - -/** - * Extract excerpt from content (first 2-3 sentences, max 150 chars) - */ -function extractExcerpt(content) { - if (!content) return ''; - - // Remove markdown formatting - let text = content - .replace(/^#+\s+/gm, '') // Remove headings - .replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold - .replace(/\*(.+?)\*/g, '$1') // Remove italic - .replace(/`(.+?)`/g, '$1') // Remove code - .replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links - .replace(/^[-*]\s+/gm, '') // Remove list markers - .replace(/^\d+\.\s+/gm, '') // Remove numbered lists - .replace(/^>\s+/gm, '') // Remove blockquotes - .replace(/\n+/g, ' ') // Collapse newlines - .trim(); - - // Get first 2-3 sentences - const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]; - let excerpt = sentences.slice(0, 2).join(' '); - - // Truncate to 150 chars if needed - if (excerpt.length > 150) { - excerpt = excerpt.substring(0, 147) + '...'; - } - - return excerpt; -} - -/** - * Estimate reading time in minutes (avg 200 words/min) - */ -function estimateReadingTime(content) { - if (!content) return 1; - - const words = content.split(/\s+/).length; - const minutes = Math.ceil(words / 200); - - return Math.max(1, minutes); -} - -/** - * Detect technical level based on content - */ -function detectTechnicalLevel(content) { - if (!content) return 'basic'; - - const lowerContent = content.toLowerCase(); - - // Technical indicators - const technicalTerms = [ - 'api', 'database', 'mongodb', 'algorithm', 'architecture', - 'implementation', 'node.js', 'javascript', 'typescript', - 'async', 'await', 'promise', 'class', 'function', - 'middleware', 'authentication', 'authorization', 'encryption', - 'hash', 'token', 'jwt', 'rest', 'graphql' - ]; - - const advancedTerms = [ - 'metacognitive', 'stochastic', 'quadrant classification', - 'intersection observer', 'csp', 'security policy', - 'cross-reference validation', 'boundary enforcement', - 'architectural constraints', 'formal verification' - ]; - - let technicalScore = 0; - let advancedScore = 0; - - // Count technical terms - technicalTerms.forEach(term => { - const regex = new RegExp(`\\b${term}\\b`, 'gi'); - const matches = lowerContent.match(regex); - if (matches) technicalScore += matches.length; - }); - - // Count advanced terms - advancedTerms.forEach(term => { - const regex = new RegExp(`\\b${term}\\b`, 'gi'); - const matches = lowerContent.match(regex); - if (matches) advancedScore += matches.length; - }); - - // Check for code blocks - const codeBlocks = (content.match(/```/g) || []).length / 2; - technicalScore += codeBlocks * 3; - - // Determine level - if (advancedScore >= 3 || technicalScore >= 15) { - return 'advanced'; - } else if (technicalScore >= 5) { - return 'intermediate'; - } else { - return 'basic'; - } -} - -/** - * Categorize section based on title and content - */ -function categorizeSection(title, content) { - const lowerTitle = title.toLowerCase(); - const lowerContent = content.toLowerCase(); - - // Category keywords - const categories = { - conceptual: [ - 'what is', 'introduction', 'overview', 'why', 'philosophy', - 'concept', 'theory', 'principle', 'background', 'motivation' - ], - technical: [ - 'architecture', 'implementation', 'technical', 'code', 'api', - 'configuration', 'setup', 'installation', 'integration', - 'class', 'function', 'service', 'component' - ], - practical: [ - 'quick start', 'tutorial', 'guide', 'how to', 'example', - 'walkthrough', 'getting started', 'usage', 'practice' - ], - reference: [ - 'reference', 'api', 'specification', 'documentation', - 'glossary', 'terms', 'definitions', 'index' - ], - critical: [ - 'security', 'warning', 'important', 'critical', 'boundary', - 'safety', 'risk', 'violation', 'error', 'failure' - ] - }; - - // Check title first (higher weight) - for (const [category, keywords] of Object.entries(categories)) { - for (const keyword of keywords) { - if (lowerTitle.includes(keyword)) { - return category; - } - } - } - - // Check content (lower weight) - const contentScores = {}; - for (const [category, keywords] of Object.entries(categories)) { - contentScores[category] = 0; - for (const keyword of keywords) { - const regex = new RegExp(`\\b${keyword}\\b`, 'gi'); - const matches = lowerContent.match(regex); - if (matches) contentScores[category] += matches.length; - } - } - - // Return category with highest score - const maxCategory = Object.keys(contentScores).reduce((a, b) => - contentScores[a] > contentScores[b] ? a : b - ); - - return contentScores[maxCategory] > 0 ? maxCategory : 'conceptual'; -} - -/** - * Generate URL-safe slug from title - */ -function generateSlug(title) { - return title - .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim(); -} - -module.exports = { - parseDocumentSections, - extractExcerpt, - estimateReadingTime, - detectTechnicalLevel, - categorizeSection, - generateSlug -}; diff --git a/src/utils/jwt.util.js b/src/utils/jwt.util.js deleted file mode 100644 index f014ef58..00000000 --- a/src/utils/jwt.util.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * JWT Utility - * Token generation and verification for admin authentication - */ - -const jwt = require('jsonwebtoken'); - -const JWT_SECRET = process.env.JWT_SECRET || 'CHANGE_THIS_IN_PRODUCTION'; -const JWT_EXPIRY = process.env.JWT_EXPIRY || '7d'; - -/** - * Generate JWT token - */ -function generateToken(payload) { - return jwt.sign(payload, JWT_SECRET, { - expiresIn: JWT_EXPIRY, - issuer: 'tractatus', - audience: 'tractatus-admin' - }); -} - -/** - * Verify JWT token - */ -function verifyToken(token) { - try { - return jwt.verify(token, JWT_SECRET, { - issuer: 'tractatus', - audience: 'tractatus-admin' - }); - } catch (error) { - throw new Error(`Invalid token: ${error.message}`); - } -} - -/** - * Decode token without verification (for debugging) - */ -function decodeToken(token) { - return jwt.decode(token); -} - -/** - * Extract token from Authorization header - */ -function extractTokenFromHeader(authHeader) { - if (!authHeader || !authHeader.startsWith('Bearer ')) { - return null; - } - return authHeader.substring(7); -} - -module.exports = { - generateToken, - verifyToken, - decodeToken, - extractTokenFromHeader -}; diff --git a/src/utils/markdown.util.js b/src/utils/markdown.util.js deleted file mode 100644 index bf731c89..00000000 --- a/src/utils/markdown.util.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Markdown Utility - * Convert markdown to HTML with syntax highlighting - */ - -const { marked } = require('marked'); -const hljs = require('highlight.js'); -const sanitizeHtml = require('sanitize-html'); - -// Custom renderer to add IDs to headings -const renderer = new marked.Renderer(); -const originalHeadingRenderer = renderer.heading.bind(renderer); - -renderer.heading = function(text, level, raw) { - // Generate slug from heading text - const slug = raw - .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim(); - - return `${text}`; -}; - -// Configure marked -marked.setOptions({ - renderer: renderer, - highlight: function(code, lang) { - if (lang && hljs.getLanguage(lang)) { - try { - return hljs.highlight(code, { language: lang }).value; - } catch (err) { - console.error('Highlight error:', err); - } - } - return hljs.highlightAuto(code).value; - }, - gfm: true, - breaks: false, - pedantic: false, - smartLists: true, - smartypants: true -}); - -/** - * Convert markdown to HTML - */ -function markdownToHtml(markdown) { - if (!markdown) return ''; - - const html = marked(markdown); - - // Sanitize HTML to prevent XSS - return sanitizeHtml(html, { - allowedTags: [ - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'p', 'br', 'hr', - 'strong', 'em', 'u', 'code', 'pre', - 'a', 'img', - 'ul', 'ol', 'li', - 'blockquote', - 'table', 'thead', 'tbody', 'tr', 'th', 'td', - 'div', 'span', - 'sup', 'sub', - 'del', 'ins' - ], - allowedAttributes: { - 'a': ['href', 'title', 'target', 'rel'], - 'img': ['src', 'alt', 'title', 'width', 'height'], - 'h1': ['id'], - 'h2': ['id'], - 'h3': ['id'], - 'h4': ['id'], - 'h5': ['id'], - 'h6': ['id'], - 'code': ['class'], - 'pre': ['class'], - 'div': ['class'], - 'span': ['class'], - 'table': ['class'], - 'th': ['scope', 'class'], - 'td': ['class'] - }, - allowedClasses: { - 'code': ['language-*', 'hljs', 'hljs-*'], - 'pre': ['hljs'], - 'div': ['highlight'], - 'span': ['hljs-*'] - } - }); -} - -/** - * Extract table of contents from markdown - */ -function extractTOC(markdown) { - if (!markdown) return []; - - const headings = []; - const lines = markdown.split('\n'); - - lines.forEach(line => { - const match = line.match(/^(#{1,6})\s+(.+)$/); - if (match) { - const level = match[1].length; - const title = match[2].replace(/[#*_`]/g, '').trim(); - const slug = title.toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim(); - - headings.push({ - level, - title, - slug - }); - } - }); - - return headings; -} - -/** - * Extract front matter from markdown - */ -function extractFrontMatter(markdown) { - if (!markdown) return { metadata: {}, content: markdown }; - - const frontMatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/; - const match = markdown.match(frontMatterRegex); - - if (!match) { - return { metadata: {}, content: markdown }; - } - - const frontMatter = match[1]; - const content = match[2]; - const metadata = {}; - - frontMatter.split('\n').forEach(line => { - const [key, ...valueParts] = line.split(':'); - if (key && valueParts.length) { - metadata[key.trim()] = valueParts.join(':').trim(); - } - }); - - return { metadata, content }; -} - -/** - * Generate slug from title - */ -function generateSlug(title) { - return title - .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim(); -} - -module.exports = { - markdownToHtml, - extractTOC, - extractFrontMatter, - generateSlug -};