fix(scripts): remove 95 accidentally published internal scripts
CRITICAL FIX: Phase 8 commit accidentally added all internal scripts to public repo
In previous commit (6efeca2), git add scripts/ added ALL internal scripts instead of
just removing the 2 project-specific scripts. This exposed internal project code.
REMOVED (95 internal scripts):
- add-*, fix-*, generate-*, migrate-*, seed-*, update-* (document/website scripts)
- import-*, load-*, query-*, verify-* (database scripts)
- audit-*, check-*, validate-* (internal validation scripts)
- archive-*, compare-*, cleanup-* (maintenance scripts)
- monitoring/* (server monitoring scripts)
- sync-instructions-to-db.js, sync-to-public.sh (internal sync scripts)
- install-*, init-koha.js, mongodb-tractatus.service (deployment scripts)
KEPT (1 script):
- scripts/clean-test-db.js (generic test database cleaner)
RESULT: Only framework-relevant scripts remain in public repo
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2af47035ac
commit
1c9892d3fe
95 changed files with 0 additions and 18679 deletions
|
|
@ -1,296 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Add API Reference documentation to database
|
||||
*
|
||||
* Creates document entries for:
|
||||
* - API Reference HTML page (links to the page)
|
||||
* - JavaScript Code Examples (markdown content)
|
||||
* - Python Code Examples (markdown content)
|
||||
* - OpenAPI Specification (download link)
|
||||
*/
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getDb } = require('../src/utils/db.util');
|
||||
const { marked } = require('marked');
|
||||
|
||||
async function addApiDocs() {
|
||||
console.log('📝 Adding API documentation to database...\n');
|
||||
|
||||
const db = await getDb();
|
||||
const collection = db.collection('documents');
|
||||
|
||||
const docs = [
|
||||
// 1. API Reference HTML Page
|
||||
{
|
||||
title: 'API Reference: Complete Endpoint Documentation',
|
||||
slug: 'api-reference-complete',
|
||||
quadrant: null,
|
||||
persistence: 'HIGH',
|
||||
audience: 'implementer',
|
||||
visibility: 'public',
|
||||
category: 'technical-reference',
|
||||
order: 10,
|
||||
content_markdown: `# API Reference: Complete Endpoint Documentation
|
||||
|
||||
Complete REST API reference for the Tractatus Framework with all endpoints, request/response examples, and authentication details.
|
||||
|
||||
**View Online:** [API Reference Page](/api-reference.html)
|
||||
|
||||
## What's Included
|
||||
|
||||
- **Authentication** - JWT-based authentication flow
|
||||
- **Documents API** - List, search, create, update documents
|
||||
- **Governance Services** - All 6 core services:
|
||||
- InstructionPersistenceClassifier
|
||||
- CrossReferenceValidator
|
||||
- BoundaryEnforcer
|
||||
- ContextPressureMonitor
|
||||
- MetacognitiveVerifier
|
||||
- AuditLogger
|
||||
- **Admin Endpoints** - Moderation queue, system stats, activity logs
|
||||
- **Error Codes** - Complete error reference with examples
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [View API Reference](/api-reference.html)
|
||||
- [Download OpenAPI Specification](/docs/api/openapi.yaml)
|
||||
- [JavaScript Examples](/docs/api/examples-javascript.md)
|
||||
- [Python Examples](/docs/api/examples-python.md)
|
||||
|
||||
## Key Features
|
||||
|
||||
✅ Complete request/response schemas
|
||||
✅ Authentication workflows
|
||||
✅ Rate limiting documentation
|
||||
✅ Error handling patterns
|
||||
✅ Lookup tables for enums
|
||||
✅ OpenAPI 3.0 specification
|
||||
`,
|
||||
content_html: null, // Will be generated from markdown
|
||||
toc: [],
|
||||
metadata: {
|
||||
author: 'John Stroh',
|
||||
date_created: new Date(),
|
||||
date_updated: new Date(),
|
||||
version: '1.0',
|
||||
document_code: 'API-REF-001',
|
||||
related_documents: ['api-js-examples', 'api-py-examples', 'openapi-spec'],
|
||||
tags: ['api', 'rest', 'endpoints', 'reference', 'openapi']
|
||||
}
|
||||
},
|
||||
|
||||
// 2. JavaScript Code Examples
|
||||
{
|
||||
title: 'JavaScript API Integration Examples',
|
||||
slug: 'api-javascript-examples',
|
||||
quadrant: null,
|
||||
persistence: 'HIGH',
|
||||
audience: 'technical',
|
||||
visibility: 'public',
|
||||
category: 'technical-reference',
|
||||
order: 11,
|
||||
content_markdown: fs.readFileSync(
|
||||
path.join(__dirname, '../docs/api/examples-javascript.md'),
|
||||
'utf8'
|
||||
),
|
||||
content_html: null, // Will be generated
|
||||
toc: [],
|
||||
metadata: {
|
||||
author: 'John Stroh',
|
||||
date_created: new Date(),
|
||||
date_updated: new Date(),
|
||||
version: '1.0',
|
||||
document_code: 'API-JS-001',
|
||||
related_documents: ['api-reference-complete', 'api-py-examples'],
|
||||
tags: ['api', 'javascript', 'nodejs', 'code-examples', 'integration']
|
||||
},
|
||||
download_formats: {
|
||||
markdown: '/docs/api/examples-javascript.md'
|
||||
}
|
||||
},
|
||||
|
||||
// 3. Python Code Examples
|
||||
{
|
||||
title: 'Python API Integration Examples',
|
||||
slug: 'api-python-examples',
|
||||
quadrant: null,
|
||||
persistence: 'HIGH',
|
||||
audience: 'technical',
|
||||
visibility: 'public',
|
||||
category: 'technical-reference',
|
||||
order: 12,
|
||||
content_markdown: fs.readFileSync(
|
||||
path.join(__dirname, '../docs/api/examples-python.md'),
|
||||
'utf8'
|
||||
),
|
||||
content_html: null, // Will be generated
|
||||
toc: [],
|
||||
metadata: {
|
||||
author: 'John Stroh',
|
||||
date_created: new Date(),
|
||||
date_updated: new Date(),
|
||||
version: '1.0',
|
||||
document_code: 'API-PY-001',
|
||||
related_documents: ['api-reference-complete', 'api-js-examples'],
|
||||
tags: ['api', 'python', 'requests', 'code-examples', 'integration']
|
||||
},
|
||||
download_formats: {
|
||||
markdown: '/docs/api/examples-python.md'
|
||||
}
|
||||
},
|
||||
|
||||
// 4. OpenAPI Specification
|
||||
{
|
||||
title: 'OpenAPI 3.0 Specification',
|
||||
slug: 'openapi-specification',
|
||||
quadrant: null,
|
||||
persistence: 'HIGH',
|
||||
audience: 'technical',
|
||||
visibility: 'public',
|
||||
category: 'technical-reference',
|
||||
order: 13,
|
||||
content_markdown: `# OpenAPI 3.0 Specification
|
||||
|
||||
Complete OpenAPI 3.0 specification for the Tractatus Framework REST API.
|
||||
|
||||
**Download:** [openapi.yaml](/docs/api/openapi.yaml)
|
||||
|
||||
## What is OpenAPI?
|
||||
|
||||
OpenAPI is a standard format for describing REST APIs. The specification can be used to:
|
||||
|
||||
- Generate interactive API documentation (Swagger UI, Redoc)
|
||||
- Auto-generate client SDKs in multiple languages
|
||||
- Validate API requests and responses
|
||||
- Mock API servers for testing
|
||||
- Import into tools like Postman, Insomnia
|
||||
|
||||
## Our Specification
|
||||
|
||||
📄 **File:** \`openapi.yaml\` (1,621 lines, 46KB)
|
||||
|
||||
**Includes:**
|
||||
- All authentication endpoints
|
||||
- Document management endpoints
|
||||
- All 6 governance service endpoints
|
||||
- Audit logging endpoints
|
||||
- Admin endpoints
|
||||
- Complete request/response schemas
|
||||
- Security definitions (JWT Bearer)
|
||||
- Error responses
|
||||
- Rate limiting details
|
||||
|
||||
## How to Use
|
||||
|
||||
### With Swagger UI
|
||||
|
||||
\`\`\`bash
|
||||
# Using npx
|
||||
npx swagger-ui-dist -u /docs/api/openapi.yaml
|
||||
|
||||
# Or with Docker
|
||||
docker run -p 8080:8080 \\
|
||||
-e SWAGGER_JSON=/docs/openapi.yaml \\
|
||||
swaggerapi/swagger-ui
|
||||
\`\`\`
|
||||
|
||||
### With Postman
|
||||
|
||||
1. Open Postman
|
||||
2. Import → Link
|
||||
3. Enter: https://agenticgovernance.digital/docs/api/openapi.yaml
|
||||
4. All endpoints will be imported with examples
|
||||
|
||||
### Generate Client SDK
|
||||
|
||||
\`\`\`bash
|
||||
# Python client
|
||||
openapi-generator generate \\
|
||||
-i /docs/api/openapi.yaml \\
|
||||
-g python \\
|
||||
-o ./tractatus-client-python
|
||||
|
||||
# TypeScript client
|
||||
openapi-generator generate \\
|
||||
-i /docs/api/openapi.yaml \\
|
||||
-g typescript-axios \\
|
||||
-o ./tractatus-client-ts
|
||||
\`\`\`
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [API Reference](/api-reference.html) - Human-readable documentation
|
||||
- [JavaScript Examples](/docs/api/examples-javascript.md)
|
||||
- [Python Examples](/docs/api/examples-python.md)
|
||||
`,
|
||||
content_html: null,
|
||||
toc: [],
|
||||
metadata: {
|
||||
author: 'John Stroh',
|
||||
date_created: new Date(),
|
||||
date_updated: new Date(),
|
||||
version: '1.0',
|
||||
document_code: 'API-SPEC-001',
|
||||
related_documents: ['api-reference-complete', 'api-js-examples', 'api-py-examples'],
|
||||
tags: ['api', 'openapi', 'swagger', 'specification', 'yaml']
|
||||
},
|
||||
download_formats: {
|
||||
yaml: '/docs/api/openapi.yaml'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
let added = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const doc of docs) {
|
||||
try {
|
||||
// Check if document already exists
|
||||
const existing = await collection.findOne({ slug: doc.slug });
|
||||
|
||||
if (existing) {
|
||||
console.log(`⏭️ Skipped (already exists): "${doc.title}"`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate HTML from markdown if not provided
|
||||
if (!doc.content_html && doc.content_markdown) {
|
||||
doc.content_html = marked.parse(doc.content_markdown);
|
||||
}
|
||||
|
||||
// Insert document
|
||||
await collection.insertOne(doc);
|
||||
console.log(`✅ Added: "${doc.title}"`);
|
||||
console.log(` Category: ${doc.category}`);
|
||||
console.log(` Audience: ${doc.audience}`);
|
||||
console.log(` Order: ${doc.order}\n`);
|
||||
added++;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error adding "${doc.title}":`, error.message);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 Summary:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`✅ Added: ${added}`);
|
||||
console.log(`⏭️ Skipped: ${skipped}`);
|
||||
console.log(`❌ Errors: ${errors}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run script
|
||||
addApiDocs().catch(error => {
|
||||
console.error('❌ Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
/**
|
||||
* Add Architectural Overview Document to MongoDB
|
||||
* Processes the architectural overview markdown and adds it to the documents collection
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const marked = require('marked');
|
||||
const { generatePdf, generatePdfHtml } = require('./generate-pdfs.js');
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
// MongoDB connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
// File paths
|
||||
const MARKDOWN_FILE = path.join(__dirname, '../docs/research/architectural-overview.md');
|
||||
const PDF_OUTPUT = path.join(__dirname, '../public/downloads/architectural-overview-and-research-status.pdf');
|
||||
|
||||
/**
|
||||
* Parse markdown content into sections
|
||||
*/
|
||||
function parseMarkdownSections(content) {
|
||||
// Remove copyright header
|
||||
content = content.replace(/<!--[\s\S]*?-->/g, '');
|
||||
|
||||
// Split by h2 headers (##)
|
||||
const sections = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
let currentSection = null;
|
||||
let currentContent = [];
|
||||
|
||||
for (const line of lines) {
|
||||
// Check for h2 header
|
||||
if (line.startsWith('## ')) {
|
||||
// Save previous section
|
||||
if (currentSection) {
|
||||
sections.push({
|
||||
title: currentSection,
|
||||
content: currentContent.join('\n').trim()
|
||||
});
|
||||
}
|
||||
|
||||
// Start new section
|
||||
currentSection = line.substring(3).trim();
|
||||
currentContent = [];
|
||||
} else if (currentSection) {
|
||||
currentContent.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save last section
|
||||
if (currentSection) {
|
||||
sections.push({
|
||||
title: currentSection,
|
||||
content: currentContent.join('\n').trim()
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate slug from title
|
||||
*/
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate reading time
|
||||
*/
|
||||
function estimateReadingTime(content) {
|
||||
const wordsPerMinute = 200;
|
||||
const wordCount = content.trim().split(/\s+/).length;
|
||||
return Math.max(1, Math.ceil(wordCount / wordsPerMinute));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract excerpt from content
|
||||
*/
|
||||
function extractExcerpt(content, maxLength = 200) {
|
||||
// Remove markdown formatting
|
||||
let text = content
|
||||
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
||||
.replace(/`[^`]+`/g, '') // Remove inline code
|
||||
.replace(/#{1,6}\s+/g, '') // Remove headers
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert links to text
|
||||
.replace(/[*_~]/g, '') // Remove formatting
|
||||
.trim();
|
||||
|
||||
// Get first paragraph or sentence
|
||||
const sentences = text.split(/[.!?]\s+/);
|
||||
let excerpt = sentences[0];
|
||||
|
||||
if (excerpt.length > maxLength) {
|
||||
excerpt = excerpt.substring(0, maxLength).trim() + '...';
|
||||
}
|
||||
|
||||
return excerpt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document sections from parsed markdown
|
||||
*/
|
||||
function createDocumentSections(parsedSections) {
|
||||
const documentSections = [];
|
||||
|
||||
for (const [index, section] of parsedSections.entries()) {
|
||||
const contentHtml = marked.parse(section.content);
|
||||
|
||||
documentSections.push({
|
||||
number: index + 1,
|
||||
title: section.title,
|
||||
slug: generateSlug(section.title),
|
||||
content_html: contentHtml,
|
||||
excerpt: extractExcerpt(section.content),
|
||||
readingTime: estimateReadingTime(section.content),
|
||||
technicalLevel: 'intermediate', // Default for architectural docs
|
||||
category: index <= 4 ? 'conceptual' : index <= 9 ? 'technical' : 'reference'
|
||||
});
|
||||
}
|
||||
|
||||
return documentSections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
console.log('=== Adding Architectural Overview Document ===\n');
|
||||
|
||||
let client;
|
||||
let browser;
|
||||
|
||||
try {
|
||||
// Read markdown file
|
||||
console.log('Reading markdown file...');
|
||||
const markdown = await fs.readFile(MARKDOWN_FILE, 'utf8');
|
||||
console.log('✓ Markdown loaded\n');
|
||||
|
||||
// Parse markdown
|
||||
console.log('Parsing markdown into sections...');
|
||||
const parsedSections = parseMarkdownSections(markdown);
|
||||
const sections = createDocumentSections(parsedSections);
|
||||
console.log(`✓ Parsed ${sections.length} sections\n`);
|
||||
|
||||
// Convert full markdown to HTML for PDF
|
||||
const fullHtml = marked.parse(markdown.replace(/<!--[\s\S]*?-->/g, ''));
|
||||
|
||||
// Create document object
|
||||
const document = {
|
||||
title: 'Tractatus Agentic Governance Framework',
|
||||
subtitle: 'Architectural Overview & Research Status',
|
||||
slug: 'architectural-overview-and-research-status',
|
||||
category: 'reference',
|
||||
excerpt: 'Comprehensive, anonymized architectural overview from inception through production-ready status. Includes system architecture, research phases, technology stack, API Memory observations, and future research directions.',
|
||||
content_html: fullHtml,
|
||||
sections: sections,
|
||||
toc: sections.map(s => ({
|
||||
title: s.title,
|
||||
slug: s.slug,
|
||||
level: 1
|
||||
})),
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
date_updated: new Date('2025-10-11'),
|
||||
date_created: new Date('2025-10-11'),
|
||||
inception_date: '2024-Q3',
|
||||
classification: 'Research Documentation',
|
||||
status: 'Production-Ready Research System',
|
||||
phase: 'Phase 5 Complete'
|
||||
},
|
||||
tags: ['architecture', 'research', 'mongodb', 'api-memory', 'production-ready'],
|
||||
order: 1, // Show first in documentation
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
// Connect to MongoDB
|
||||
console.log('Connecting to MongoDB...');
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
console.log('✓ Connected\n');
|
||||
|
||||
// Insert or update document
|
||||
console.log('Saving document to MongoDB...');
|
||||
const result = await db.collection('documents').updateOne(
|
||||
{ slug: document.slug },
|
||||
{ $set: document },
|
||||
{ upsert: true }
|
||||
);
|
||||
|
||||
if (result.upsertedCount > 0) {
|
||||
console.log('✓ Document inserted\n');
|
||||
} else {
|
||||
console.log('✓ Document updated\n');
|
||||
}
|
||||
|
||||
// Generate PDF
|
||||
console.log('Generating PDF...');
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const html = generatePdfHtml(document);
|
||||
await page.setContent(html, { waitUntil: 'networkidle0' });
|
||||
|
||||
await page.pdf({
|
||||
path: PDF_OUTPUT,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '2cm',
|
||||
right: '2cm',
|
||||
bottom: '2cm',
|
||||
left: '2cm'
|
||||
},
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; font-size: 9pt; text-align: center; color: #6b7280; padding: 0 2cm;">
|
||||
<span class="pageNumber"></span> / <span class="totalPages"></span>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
console.log(`✓ PDF generated: ${path.basename(PDF_OUTPUT)}\n`);
|
||||
|
||||
// Summary
|
||||
console.log('=== Complete ===\n');
|
||||
console.log(`Document: ${document.title}`);
|
||||
console.log(`Sections: ${sections.length}`);
|
||||
console.log(`Slug: ${document.slug}`);
|
||||
console.log(`Version: ${document.metadata.version}`);
|
||||
console.log(`\nPDF: public/downloads/${path.basename(PDF_OUTPUT)}`);
|
||||
console.log(`MongoDB: documents collection (${result.upsertedCount > 0 ? 'inserted' : 'updated'})`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Add event delegation to remaining admin files
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// project-editor.js
|
||||
const projectEditorFile = path.join(__dirname, '../public/js/admin/project-editor.js');
|
||||
let projectEditorContent = fs.readFileSync(projectEditorFile, 'utf8');
|
||||
|
||||
const projectEditorDelegation = `
|
||||
// Event delegation for data-action buttons (CSP compliance)
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const arg0 = button.dataset.arg0;
|
||||
|
||||
if (action === 'editVariable') {
|
||||
window.projectEditor.editVariable(arg0);
|
||||
} else if (action === 'deleteVariable') {
|
||||
window.projectEditor.deleteVariable(arg0);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
if (!projectEditorContent.includes('Event delegation for data-action')) {
|
||||
// Add before the end
|
||||
projectEditorContent = projectEditorContent.trim() + '\n' + projectEditorDelegation;
|
||||
fs.writeFileSync(projectEditorFile, projectEditorContent);
|
||||
console.log('✓ Added event delegation to project-editor.js');
|
||||
}
|
||||
|
||||
// rule-editor.js
|
||||
const ruleEditorFile = path.join(__dirname, '../public/js/admin/rule-editor.js');
|
||||
let ruleEditorContent = fs.readFileSync(ruleEditorFile, 'utf8');
|
||||
|
||||
const ruleEditorDelegation = `
|
||||
// Event delegation for data-action buttons (CSP compliance)
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const arg0 = button.dataset.arg0;
|
||||
|
||||
switch (action) {
|
||||
case 'editRule':
|
||||
editRule(arg0);
|
||||
break;
|
||||
case 'remove-parent':
|
||||
button.parentElement.remove();
|
||||
break;
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
if (!ruleEditorContent.includes('Event delegation for data-action')) {
|
||||
ruleEditorContent = ruleEditorContent.trim() + '\n' + ruleEditorDelegation;
|
||||
fs.writeFileSync(ruleEditorFile, ruleEditorContent);
|
||||
console.log('✓ Added event delegation to rule-editor.js');
|
||||
}
|
||||
|
||||
// audit-analytics.js
|
||||
const auditFile = path.join(__dirname, '../public/js/admin/audit-analytics.js');
|
||||
let auditContent = fs.readFileSync(auditFile, 'utf8');
|
||||
|
||||
const auditDelegation = `
|
||||
// Event delegation for data-action buttons (CSP compliance)
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const arg0 = button.dataset.arg0;
|
||||
|
||||
if (action === 'showDecisionDetails') {
|
||||
showDecisionDetails(arg0);
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
if (!auditContent.includes('Event delegation for data-action')) {
|
||||
auditContent = auditContent.trim() + '\n' + auditDelegation;
|
||||
fs.writeFileSync(auditFile, auditContent);
|
||||
console.log('✓ Added event delegation to audit-analytics.js');
|
||||
}
|
||||
|
||||
// claude-md-migrator.js
|
||||
const migratorFile = path.join(__dirname, '../public/js/admin/claude-md-migrator.js');
|
||||
let migratorContent = fs.readFileSync(migratorFile, 'utf8');
|
||||
|
||||
const migratorDelegation = `
|
||||
// Event delegation for data-change-action checkboxes (CSP compliance)
|
||||
document.addEventListener('change', (e) => {
|
||||
const checkbox = e.target.closest('[data-change-action]');
|
||||
if (!checkbox) return;
|
||||
|
||||
const action = checkbox.dataset.changeAction;
|
||||
const index = parseInt(checkbox.dataset.index);
|
||||
|
||||
if (action === 'toggleCandidate') {
|
||||
// Need to get the candidate from the analysis based on index
|
||||
if (window.currentAnalysis && window.currentAnalysis.candidates[index]) {
|
||||
toggleCandidate(window.currentAnalysis.candidates[index], checkbox.checked);
|
||||
}
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
if (!migratorContent.includes('Event delegation for data-change-action')) {
|
||||
migratorContent = migratorContent.trim() + '\n' + migratorDelegation;
|
||||
fs.writeFileSync(migratorFile, migratorContent);
|
||||
console.log('✓ Added event delegation to claude-md-migrator.js');
|
||||
}
|
||||
|
||||
console.log('\n✅ Event delegation added to all remaining admin files\n');
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Add inst_026 and inst_027 to Governance Rules Database
|
||||
* These rules emerged from blog implementation validation
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const GovernanceRule = require('../src/models/GovernanceRule.model');
|
||||
|
||||
// Connect to MongoDB
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
|
||||
const rules = [
|
||||
{
|
||||
id: 'inst_026',
|
||||
text: `Client-Side Code Quality Standards (OPERATIONAL)
|
||||
|
||||
All client-side JavaScript (public/js/**) must adhere to these quality standards:
|
||||
|
||||
1. **Framework Usage**: Use vanilla JavaScript unless framework is explicitly approved
|
||||
- No React, Vue, Angular without approval
|
||||
- Prefer native DOM APIs
|
||||
- Minimize external dependencies
|
||||
|
||||
2. **XSS Prevention**: Include HTML escaping for all user-generated content
|
||||
- Implement escapeHtml() function
|
||||
- Use textContent instead of innerHTML where possible
|
||||
- Sanitize all dynamic content before rendering
|
||||
|
||||
3. **URL Portability**: Use relative URLs (no hardcoded hosts)
|
||||
- ✓ Good: "/api/blog", "/blog.html"
|
||||
- ✗ Bad: "http://localhost:9000/api/blog", "https://agenticgovernance.digital/blog.html"
|
||||
- Ensures code works in dev, staging, and production
|
||||
|
||||
4. **Performance**: Implement debouncing for search inputs
|
||||
- Minimum 300ms debounce for search/filter inputs
|
||||
- Prevents excessive API calls and DOM updates
|
||||
- Use setTimeout/clearTimeout pattern
|
||||
|
||||
5. **Event Handling**: Use event delegation for dynamic elements
|
||||
- Attach listeners to parent containers
|
||||
- Use event.target.closest() for delegation
|
||||
- Prevents memory leaks from repeated listener attachment
|
||||
|
||||
6. **User Experience**: Include loading, error, and empty states
|
||||
- Loading: Spinner or skeleton UI
|
||||
- Error: User-friendly error message with recovery action
|
||||
- Empty: Helpful message explaining why no data exists
|
||||
|
||||
7. **Linting**: Pass ESLint validation with zero warnings
|
||||
- Run: npx eslint <file> --max-warnings 0
|
||||
- Fix all auto-fixable issues
|
||||
- Manually resolve remaining warnings
|
||||
|
||||
**Validation**:
|
||||
- Check for escapeHtml() function
|
||||
- grep for hardcoded URLs (localhost, production domain)
|
||||
- Verify debounce implementation on search inputs
|
||||
- Confirm event delegation usage
|
||||
- Run ESLint with --max-warnings 0
|
||||
|
||||
**Boundary Classification**: TECHNICAL (safe for automation)
|
||||
These are objective, testable code quality standards with no values component.`,
|
||||
|
||||
quadrant: 'OPERATIONAL',
|
||||
persistence: 'MEDIUM',
|
||||
scope: 'PROJECT_SPECIFIC',
|
||||
applicableProjects: ['tractatus'],
|
||||
category: 'technical',
|
||||
priority: 70,
|
||||
temporalScope: 'PROJECT',
|
||||
source: 'user_instruction',
|
||||
createdBy: 'claude_code',
|
||||
active: true,
|
||||
notes: 'Created after blog implementation validation. Emerged from CSP violations in navbar.js and need for consistent client-side code quality.',
|
||||
examples: [
|
||||
'XSS Prevention: function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; }',
|
||||
'Debouncing: let timeout; input.addEventListener("input", (e) => { clearTimeout(timeout); timeout = setTimeout(() => filter(e.target.value), 300); });',
|
||||
'Event Delegation: container.addEventListener("click", (e) => { const btn = e.target.closest(".btn"); if (btn) handleClick(btn); });'
|
||||
],
|
||||
relatedRules: ['inst_008', 'inst_027']
|
||||
},
|
||||
|
||||
{
|
||||
id: 'inst_027',
|
||||
text: `Production Deployment Checklist (TACTICAL)
|
||||
|
||||
Before deploying to production, verify ALL of the following:
|
||||
|
||||
**1. Code Cleanliness**
|
||||
- [ ] No console.log() statements (console.error() allowed for error handling)
|
||||
- [ ] No console.debug(), console.warn() in production code
|
||||
- [ ] No TODO, FIXME, DEBUG, HACK, or XXX comments
|
||||
- [ ] No commented-out code blocks
|
||||
|
||||
**2. Environment Independence**
|
||||
- [ ] No hardcoded localhost URLs
|
||||
- [ ] No hardcoded production URLs (use relative paths)
|
||||
- [ ] No hardcoded IP addresses
|
||||
- [ ] Environment variables used for configuration
|
||||
|
||||
**3. Security Validation**
|
||||
- [ ] CSP compliance (inst_008) validated on all HTML/JS files
|
||||
- [ ] No inline event handlers (onclick, onload, etc.)
|
||||
- [ ] No inline styles (use CSS classes)
|
||||
- [ ] No inline scripts
|
||||
- [ ] No javascript: URLs
|
||||
|
||||
**4. File Organization**
|
||||
- [ ] All files in production-ready locations (public/, src/)
|
||||
- [ ] No temporary files (.tmp, .bak, ~)
|
||||
- [ ] No development-only files
|
||||
- [ ] .rsyncignore excludes sensitive files
|
||||
|
||||
**5. Cache Busting**
|
||||
- [ ] CSS version parameter updated (?v=TIMESTAMP)
|
||||
- [ ] JavaScript version parameter updated (?v=TIMESTAMP)
|
||||
- [ ] Image version parameters if needed
|
||||
|
||||
**6. Sensitive Data Protection**
|
||||
- [ ] .env files NOT included in deployment
|
||||
- [ ] CLAUDE.md NOT included (verify in .rsyncignore)
|
||||
- [ ] Session state (.claude/) NOT included
|
||||
- [ ] No API keys, secrets, or credentials in code
|
||||
|
||||
**7. Testing**
|
||||
- [ ] Manual testing in development environment
|
||||
- [ ] All API endpoints return expected responses
|
||||
- [ ] Error states display correctly
|
||||
- [ ] Loading states work
|
||||
- [ ] Mobile responsive layout verified
|
||||
|
||||
**Validation Commands**:
|
||||
\`\`\`bash
|
||||
# Check for console statements
|
||||
grep -r "console\\.log" public/ || echo "✓ No console.log found"
|
||||
|
||||
# Check for development comments
|
||||
grep -r "TODO\\|FIXME\\|DEBUG" public/ || echo "✓ No dev comments found"
|
||||
|
||||
# Check for hardcoded URLs
|
||||
grep -r "localhost\\|http://\\|https://" public/ | grep -v ".html" || echo "✓ No hardcoded URLs found"
|
||||
|
||||
# Verify CSP compliance
|
||||
node scripts/pre-action-check.js file-edit public/index.html "Deployment validation"
|
||||
|
||||
# Verify .rsyncignore coverage
|
||||
grep "CLAUDE.md" .rsyncignore && grep ".claude/" .rsyncignore && echo "✓ Sensitive files excluded"
|
||||
\`\`\`
|
||||
|
||||
**Deployment Process**:
|
||||
1. Run all validation commands above
|
||||
2. Execute: ./scripts/deploy-full-project-SAFE.sh
|
||||
3. Review dry-run output carefully
|
||||
4. Confirm deployment
|
||||
5. SSH to production and verify sensitive files NOT deployed
|
||||
6. Restart service: sudo systemctl restart tractatus
|
||||
7. Test production site: https://agenticgovernance.digital
|
||||
|
||||
**Boundary Classification**: TECHNICAL (automated checklist)
|
||||
All checks are objective and can be automated. No values decisions required.`,
|
||||
|
||||
quadrant: 'TACTICAL',
|
||||
persistence: 'HIGH',
|
||||
scope: 'UNIVERSAL',
|
||||
applicableProjects: ['*'],
|
||||
category: 'process',
|
||||
priority: 85,
|
||||
temporalScope: 'PERMANENT',
|
||||
source: 'user_instruction',
|
||||
createdBy: 'claude_code',
|
||||
active: true,
|
||||
notes: 'Created after blog implementation validation. Prevents common deployment errors like console.log statements, hardcoded URLs, and CSP violations from reaching production.',
|
||||
examples: [
|
||||
'Pre-deployment validation: grep -r "console.log" public/ && echo "FAIL: console.log found" && exit 1',
|
||||
'CSP validation: node scripts/pre-action-check.js file-edit public/blog.html "Deployment check"',
|
||||
'Sensitive file check: ssh production "ls /var/www/tractatus/CLAUDE.md 2>/dev/null && echo FAIL || echo OK"'
|
||||
],
|
||||
relatedRules: ['inst_008', 'inst_026']
|
||||
}
|
||||
];
|
||||
|
||||
async function addRules() {
|
||||
try {
|
||||
console.log('Connecting to MongoDB:', MONGODB_URI);
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log('✓ Connected to MongoDB\n');
|
||||
|
||||
for (const rule of rules) {
|
||||
console.log(`Adding ${rule.id}...`);
|
||||
|
||||
// Check if rule already exists
|
||||
const existing = await GovernanceRule.findOne({ id: rule.id });
|
||||
|
||||
if (existing) {
|
||||
console.log(` ⚠ Rule ${rule.id} already exists. Updating...`);
|
||||
await GovernanceRule.updateOne({ id: rule.id }, rule);
|
||||
console.log(` ✓ Updated ${rule.id}`);
|
||||
} else {
|
||||
await GovernanceRule.create(rule);
|
||||
console.log(` ✓ Created ${rule.id}`);
|
||||
}
|
||||
|
||||
console.log(` Quadrant: ${rule.quadrant}`);
|
||||
console.log(` Persistence: ${rule.persistence}`);
|
||||
console.log(` Scope: ${rule.scope}`);
|
||||
console.log(` Priority: ${rule.priority}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('✓ All rules added successfully!\n');
|
||||
|
||||
// Show summary
|
||||
const allRules = await GovernanceRule.find({ active: true }).sort({ id: 1 });
|
||||
console.log(`Total active rules: ${allRules.length}`);
|
||||
console.log('Rule IDs:', allRules.map(r => r.id).join(', '));
|
||||
|
||||
await mongoose.disconnect();
|
||||
console.log('\n✓ Disconnected from MongoDB');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding rules:', error);
|
||||
await mongoose.disconnect();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addRules();
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Add setProgressBarWidths helper and calls
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const helper = `
|
||||
// Set widths/heights from data attributes (CSP compliance)
|
||||
function setProgressBarWidths(container) {
|
||||
const elements = container.querySelectorAll('[data-width], [data-height]');
|
||||
elements.forEach(el => {
|
||||
if (el.dataset.width) el.style.width = el.dataset.width + '%';
|
||||
if (el.dataset.height) el.style.height = el.dataset.height + '%';
|
||||
});
|
||||
}`;
|
||||
|
||||
// audit-analytics.js
|
||||
const auditFile = path.join(__dirname, '../public/js/admin/audit-analytics.js');
|
||||
let auditContent = fs.readFileSync(auditFile, 'utf8');
|
||||
|
||||
if (!auditContent.includes('setProgressBarWidths')) {
|
||||
// Add helper before last })
|
||||
const lastBrace = auditContent.lastIndexOf('})();');
|
||||
auditContent = auditContent.slice(0, lastBrace) + helper + '\n' + auditContent.slice(lastBrace);
|
||||
|
||||
// Add calls after innerHTML assignments with progress bars
|
||||
auditContent = auditContent.replace(
|
||||
/chartEl\.innerHTML = html;/g,
|
||||
'chartEl.innerHTML = html; setProgressBarWidths(chartEl);'
|
||||
);
|
||||
auditContent = auditContent.replace(
|
||||
/chartEl\.innerHTML = `<div class="flex items-end gap-2 h-full">\$\{html\}<\/div>`;/g,
|
||||
'chartEl.innerHTML = `<div class="flex items-end gap-2 h-full">${html}</div>`; setProgressBarWidths(chartEl);'
|
||||
);
|
||||
|
||||
fs.writeFileSync(auditFile, auditContent);
|
||||
console.log('✓ Fixed audit-analytics.js');
|
||||
}
|
||||
|
||||
// rule-manager.js
|
||||
const ruleManagerFile = path.join(__dirname, '../public/js/admin/rule-manager.js');
|
||||
let ruleManagerContent = fs.readFileSync(ruleManagerFile, 'utf8');
|
||||
|
||||
if (!ruleManagerContent.includes('setProgressBarWidths')) {
|
||||
// Add helper before last })
|
||||
const lastBrace = ruleManagerContent.lastIndexOf('})();');
|
||||
ruleManagerContent = ruleManagerContent.slice(0, lastBrace) + helper + '\n' + ruleManagerContent.slice(lastBrace);
|
||||
|
||||
// Add calls after container.innerHTML assignments
|
||||
ruleManagerContent = ruleManagerContent.replace(
|
||||
/(container\.innerHTML = `[\s\S]*?`;)/g,
|
||||
'$1 setProgressBarWidths(container);'
|
||||
);
|
||||
|
||||
fs.writeFileSync(ruleManagerFile, ruleManagerContent);
|
||||
console.log('✓ Fixed rule-manager.js');
|
||||
}
|
||||
|
||||
// rule-editor.js
|
||||
const ruleEditorFile = path.join(__dirname, '../public/js/admin/rule-editor.js');
|
||||
let ruleEditorContent = fs.readFileSync(ruleEditorFile, 'utf8');
|
||||
|
||||
if (!ruleEditorContent.includes('setProgressBarWidths')) {
|
||||
// Add helper before last })
|
||||
const lastBrace = ruleEditorContent.lastIndexOf('})();');
|
||||
ruleEditorContent = ruleEditorContent.slice(0, lastBrace) + helper + '\n' + ruleEditorContent.slice(lastBrace);
|
||||
|
||||
// Add calls after modal content is set
|
||||
ruleEditorContent = ruleEditorContent.replace(
|
||||
/(modalContent\.innerHTML = `[\s\S]*?`;)(\s+\/\/ Show modal)/g,
|
||||
'$1 setProgressBarWidths(modalContent);$2'
|
||||
);
|
||||
|
||||
fs.writeFileSync(ruleEditorFile, ruleEditorContent);
|
||||
console.log('✓ Fixed rule-editor.js');
|
||||
}
|
||||
|
||||
console.log('\n✅ Progress bar helpers added\n');
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Add Card View Sections to Documents (Using DB Markdown)
|
||||
*
|
||||
* Generates sections from the content_markdown field stored in the database
|
||||
* for documents that don't have corresponding MD files on disk.
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { marked } = require('marked');
|
||||
|
||||
// List of document slugs that need sections
|
||||
const SLUGS_NEEDING_SECTIONS = [
|
||||
// 5 newly imported archives
|
||||
'case-studies-real-world-llm-failure-modes-appendix',
|
||||
'implementation-guide-python-examples',
|
||||
'tractatus-framework-enforcement-claude-code',
|
||||
'research-topic-concurrent-session-architecture',
|
||||
'research-topic-rule-proliferation-transactional-overhead',
|
||||
|
||||
// 5 technical reference docs
|
||||
'implementation-roadmap-24-month-deployment-plan',
|
||||
'api-reference-complete',
|
||||
'api-javascript-examples',
|
||||
'api-python-examples',
|
||||
'openapi-specification',
|
||||
|
||||
// 5 case studies
|
||||
'the-27027-incident-a-case-study-in-pattern-recognition-bias',
|
||||
'when-frameworks-fail-and-why-thats-ok',
|
||||
'our-framework-in-action-detecting-and-correcting-ai-fabrications',
|
||||
'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery',
|
||||
'case-studies-real-world-llm-failure-modes',
|
||||
|
||||
// 2 Phase 5 PoC summaries
|
||||
'phase-5-poc-session-1-summary',
|
||||
'phase-5-poc-session-2-summary'
|
||||
];
|
||||
|
||||
function extractSectionsFromMarkdown(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const sections = [];
|
||||
let currentSection = null;
|
||||
let contentBuffer = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Match H2 headers (## Title)
|
||||
const h2Match = line.match(/^## (.+)$/);
|
||||
if (h2Match) {
|
||||
// Save previous section if exists
|
||||
if (currentSection) {
|
||||
currentSection.content_md = contentBuffer.join('\n').trim();
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
// Start new section
|
||||
currentSection = {
|
||||
title: h2Match[1].trim(),
|
||||
content_md: ''
|
||||
};
|
||||
contentBuffer = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect content for current section
|
||||
if (currentSection) {
|
||||
contentBuffer.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save final section
|
||||
if (currentSection) {
|
||||
currentSection.content_md = contentBuffer.join('\n').trim();
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
function generateExcerpt(markdown, maxLength = 150) {
|
||||
let text = markdown
|
||||
.replace(/^#+\s+/gm, '')
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/\[(.+?)\]\(.+?\)/g, '$1')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/^[-*+]\s+/gm, '')
|
||||
.replace(/^\d+\.\s+/gm, '')
|
||||
.replace(/\n{2,}/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (text.length > maxLength) {
|
||||
text = text.substring(0, maxLength).trim();
|
||||
const lastPeriod = text.lastIndexOf('.');
|
||||
if (lastPeriod > maxLength * 0.7) {
|
||||
text = text.substring(0, lastPeriod + 1);
|
||||
} else {
|
||||
text += '...';
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function estimateReadingTime(text) {
|
||||
const wordCount = text.split(/\s+/).length;
|
||||
const minutes = Math.ceil(wordCount / 200);
|
||||
return Math.max(1, minutes);
|
||||
}
|
||||
|
||||
function classifySection(title, content) {
|
||||
const titleLower = title.toLowerCase();
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
if (
|
||||
titleLower.includes('limitation') ||
|
||||
titleLower.includes('failure') ||
|
||||
titleLower.includes('warning') ||
|
||||
titleLower.includes('security') ||
|
||||
titleLower.includes('risk') ||
|
||||
content.match(/⚠️|critical|warning|caution|danger/gi)
|
||||
) {
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
if (
|
||||
titleLower.includes('glossary') ||
|
||||
titleLower.includes('reference') ||
|
||||
titleLower.includes('contact') ||
|
||||
titleLower.includes('license') ||
|
||||
titleLower.includes('getting started')
|
||||
) {
|
||||
return 'reference';
|
||||
}
|
||||
|
||||
if (
|
||||
titleLower.includes('technical') ||
|
||||
titleLower.includes('architecture') ||
|
||||
titleLower.includes('implementation') ||
|
||||
titleLower.includes('integration') ||
|
||||
titleLower.includes('api') ||
|
||||
content.match(/```|`[a-z]+`|function|class|const|import/gi)
|
||||
) {
|
||||
return 'technical';
|
||||
}
|
||||
|
||||
if (
|
||||
titleLower.includes('how') ||
|
||||
titleLower.includes('guide') ||
|
||||
titleLower.includes('tutorial') ||
|
||||
titleLower.includes('example') ||
|
||||
titleLower.includes('use case') ||
|
||||
titleLower.includes('should use') ||
|
||||
titleLower.includes('contributing')
|
||||
) {
|
||||
return 'practical';
|
||||
}
|
||||
|
||||
return 'conceptual';
|
||||
}
|
||||
|
||||
function determineTechnicalLevel(content) {
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
if (
|
||||
content.match(/```[\s\S]+```/g) ||
|
||||
contentLower.includes('api') ||
|
||||
contentLower.includes('implementation') ||
|
||||
contentLower.includes('integration') ||
|
||||
contentLower.includes('architecture')
|
||||
) {
|
||||
return 'advanced';
|
||||
}
|
||||
|
||||
if (
|
||||
contentLower.includes('service') ||
|
||||
contentLower.includes('component') ||
|
||||
contentLower.includes('system') ||
|
||||
contentLower.includes('framework')
|
||||
) {
|
||||
return 'intermediate';
|
||||
}
|
||||
|
||||
return 'beginner';
|
||||
}
|
||||
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
async function addSectionsToDocument(slug) {
|
||||
console.log(`\n📄 Processing: ${slug}`);
|
||||
|
||||
try {
|
||||
// Find document
|
||||
const doc = await Document.findBySlug(slug);
|
||||
if (!doc) {
|
||||
console.log(` ❌ Document not found`);
|
||||
return { success: false, reason: 'not_found' };
|
||||
}
|
||||
|
||||
// Check if already has sections
|
||||
if (doc.sections && doc.sections.length > 0) {
|
||||
console.log(` ⏭️ Already has ${doc.sections.length} sections`);
|
||||
return { success: false, reason: 'has_sections' };
|
||||
}
|
||||
|
||||
// Check if has content_markdown
|
||||
if (!doc.content_markdown) {
|
||||
console.log(` ❌ No content_markdown field`);
|
||||
return { success: false, reason: 'no_markdown' };
|
||||
}
|
||||
|
||||
// Extract sections from markdown
|
||||
const rawSections = extractSectionsFromMarkdown(doc.content_markdown);
|
||||
|
||||
if (rawSections.length === 0) {
|
||||
console.log(` ⚠️ No H2 sections found in markdown`);
|
||||
return { success: false, reason: 'no_h2' };
|
||||
}
|
||||
|
||||
console.log(` 📝 Found ${rawSections.length} sections`);
|
||||
|
||||
// Process each section
|
||||
const sections = [];
|
||||
for (let i = 0; i < rawSections.length; i++) {
|
||||
const raw = rawSections[i];
|
||||
|
||||
if (!raw.content_md.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content_html = marked(raw.content_md);
|
||||
const excerpt = generateExcerpt(raw.content_md);
|
||||
const readingTime = estimateReadingTime(raw.content_md);
|
||||
const category = classifySection(raw.title, raw.content_md);
|
||||
const technicalLevel = determineTechnicalLevel(raw.content_md);
|
||||
const sectionSlug = generateSlug(raw.title);
|
||||
|
||||
sections.push({
|
||||
number: i + 1,
|
||||
title: raw.title,
|
||||
slug: sectionSlug,
|
||||
content_html,
|
||||
excerpt,
|
||||
readingTime,
|
||||
technicalLevel,
|
||||
category
|
||||
});
|
||||
}
|
||||
|
||||
// Update document
|
||||
const updated = await Document.update(doc._id.toString(), { sections });
|
||||
|
||||
if (!updated) {
|
||||
console.log(` ❌ Failed to update`);
|
||||
return { success: false, reason: 'update_failed' };
|
||||
}
|
||||
|
||||
console.log(` ✅ Added ${sections.length} sections`);
|
||||
sections.forEach(s => {
|
||||
console.log(` ${s.number}. ${s.title} (${s.category}, ${s.readingTime}min)`);
|
||||
});
|
||||
|
||||
return { success: true, sections: sections.length };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🚀 Adding Card View Sections to 17 Documents\n');
|
||||
console.log('═══════════════════════════════════════════════════\n');
|
||||
|
||||
await connect();
|
||||
|
||||
let added = 0;
|
||||
let skipped = 0;
|
||||
let noH2 = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const slug of SLUGS_NEEDING_SECTIONS) {
|
||||
const result = await addSectionsToDocument(slug);
|
||||
|
||||
if (result.success) {
|
||||
added++;
|
||||
} else if (result.reason === 'has_sections') {
|
||||
skipped++;
|
||||
} else if (result.reason === 'no_h2') {
|
||||
noH2++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════');
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` ✅ Added sections: ${added}`);
|
||||
console.log(` ⏭️ Skipped (already have sections): ${skipped}`);
|
||||
console.log(` ⚠️ No H2 sections found: ${noH2}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📦 Total: ${SLUGS_NEEDING_SECTIONS.length}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Add Card View Sections to 17 Documents
|
||||
*
|
||||
* Adds card view sections to:
|
||||
* - 5 newly imported archives
|
||||
* - 12 existing documents without sections
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { processMarkdownFile } = require('./generate-card-sections.js');
|
||||
|
||||
const DOCS_TO_PROCESS = [
|
||||
// 5 newly imported archives
|
||||
{ slug: 'case-studies-real-world-llm-failure-modes-appendix', mdPath: 'docs/markdown/case-studies.md' },
|
||||
{ slug: 'implementation-guide-python-examples', mdPath: 'docs/api/examples-python.md' },
|
||||
{ slug: 'tractatus-framework-enforcement-claude-code', mdPath: 'docs/claude-code-framework-enforcement.md' },
|
||||
{ slug: 'research-topic-concurrent-session-architecture', mdPath: 'docs/research/concurrent-session-architecture-limitations.md' },
|
||||
{ slug: 'research-topic-rule-proliferation-transactional-overhead', mdPath: 'docs/research/rule-proliferation-and-transactional-overhead.md' },
|
||||
|
||||
// 5 technical reference docs without sections
|
||||
{ slug: 'implementation-roadmap-24-month-deployment-plan', mdPath: 'docs/markdown/implementation-roadmap-24-month-deployment-plan.md' },
|
||||
{ slug: 'api-reference-complete', mdPath: 'docs/markdown/api-reference-complete.md' },
|
||||
{ slug: 'api-javascript-examples', mdPath: 'docs/api/examples-javascript.md' },
|
||||
{ slug: 'api-python-examples', mdPath: 'docs/api/examples-python.md' },
|
||||
{ slug: 'openapi-specification', mdPath: 'docs/markdown/openapi-specification.md' },
|
||||
|
||||
// 5 case studies without sections
|
||||
{ slug: 'the-27027-incident-a-case-study-in-pattern-recognition-bias', mdPath: 'docs/case-studies/27027-incident-detailed-analysis.md' },
|
||||
{ slug: 'when-frameworks-fail-and-why-thats-ok', mdPath: 'docs/case-studies/when-frameworks-fail-oct-2025.md' },
|
||||
{ slug: 'our-framework-in-action-detecting-and-correcting-ai-fabrications', mdPath: 'docs/case-studies/framework-in-action-oct-2025.md' },
|
||||
{ slug: 'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery', mdPath: 'docs/case-studies/real-world-governance-case-study-oct-2025.md' },
|
||||
{ slug: 'case-studies-real-world-llm-failure-modes', mdPath: 'docs/markdown/case-studies.md' },
|
||||
|
||||
// 2 Phase 5 PoC summaries
|
||||
{ slug: 'phase-5-poc-session-1-summary', mdPath: 'docs/markdown/phase-5-session1-summary.md' },
|
||||
{ slug: 'phase-5-poc-session-2-summary', mdPath: 'docs/markdown/phase-5-session2-summary.md' }
|
||||
];
|
||||
|
||||
async function addSectionsToDocument(docInfo) {
|
||||
console.log(`\n📄 Processing: ${docInfo.slug}`);
|
||||
|
||||
try {
|
||||
// Check if document exists
|
||||
const doc = await Document.findBySlug(docInfo.slug);
|
||||
if (!doc) {
|
||||
console.log(` ❌ Document not found in database`);
|
||||
return { success: false, reason: 'not_found' };
|
||||
}
|
||||
|
||||
// Check if already has sections
|
||||
if (doc.sections && doc.sections.length > 0) {
|
||||
console.log(` ⏭️ Already has ${doc.sections.length} sections, skipping`);
|
||||
return { success: false, reason: 'has_sections' };
|
||||
}
|
||||
|
||||
// Build full path to markdown file
|
||||
const fullPath = path.join('/home/theflow/projects/tractatus', docInfo.mdPath);
|
||||
|
||||
// Check if markdown file exists
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(` ❌ Markdown file not found: ${fullPath}`);
|
||||
return { success: false, reason: 'md_not_found' };
|
||||
}
|
||||
|
||||
// Generate sections
|
||||
console.log(` 📝 Generating sections from: ${docInfo.mdPath}`);
|
||||
const sections = await processMarkdownFile(fullPath);
|
||||
|
||||
if (!sections || sections.length === 0) {
|
||||
console.log(` ⚠️ No sections generated (possibly no H2 headers)`);
|
||||
return { success: false, reason: 'no_sections' };
|
||||
}
|
||||
|
||||
// Update document with sections
|
||||
const updated = await Document.update(doc._id.toString(), { sections });
|
||||
|
||||
if (!updated) {
|
||||
console.log(` ❌ Failed to update document`);
|
||||
return { success: false, reason: 'update_failed' };
|
||||
}
|
||||
|
||||
console.log(` ✅ Added ${sections.length} sections`);
|
||||
return { success: true, sections: sections.length };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🚀 Adding Card View Sections to 17 Documents\n');
|
||||
console.log('═══════════════════════════════════════════════════\n');
|
||||
|
||||
await connect();
|
||||
|
||||
let added = 0;
|
||||
let skipped = 0;
|
||||
let notFound = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const docInfo of DOCS_TO_PROCESS) {
|
||||
const result = await addSectionsToDocument(docInfo);
|
||||
|
||||
if (result.success) {
|
||||
added++;
|
||||
} else if (result.reason === 'has_sections') {
|
||||
skipped++;
|
||||
} else if (result.reason === 'not_found' || result.reason === 'md_not_found') {
|
||||
notFound++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════');
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` ✅ Added sections: ${added}`);
|
||||
console.log(` ⏭️ Skipped (already have sections): ${skipped}`);
|
||||
console.log(` ❌ Not found: ${notFound}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📦 Total processed: ${DOCS_TO_PROCESS.length}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/**
|
||||
* Add section-based cards to all existing documents
|
||||
* Parses documents and adds sections array for card-based UI
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { MongoClient } = require('mongodb');
|
||||
const { parseDocumentSections } = require('../src/utils/document-section-parser');
|
||||
const { markdownToHtml } = require('../src/utils/markdown.util');
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_prod';
|
||||
const DOCS_DIR = process.env.DOCS_DIR || '/var/www/tractatus/docs/markdown';
|
||||
|
||||
async function main() {
|
||||
console.log('=== Adding Sections to Documents ===\n');
|
||||
|
||||
const client = new MongoClient(MONGODB_URI);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db();
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Get all documents
|
||||
const documents = await collection.find({}).toArray();
|
||||
console.log(`Found ${documents.length} documents in database\n`);
|
||||
|
||||
const markdownFiles = {
|
||||
'tractatus-agentic-governance-system-glossary-of-terms': 'GLOSSARY.md',
|
||||
'introduction-to-the-tractatus-framework': 'introduction.md',
|
||||
'core-concepts-of-the-tractatus-framework': 'core-concepts.md',
|
||||
'implementation-guide': 'implementation-guide.md',
|
||||
'case-studies-real-world-llm-failure-modes': 'case-studies.md'
|
||||
};
|
||||
|
||||
for (const doc of documents) {
|
||||
console.log(`Processing: ${doc.title}`);
|
||||
|
||||
const markdownFile = markdownFiles[doc.slug];
|
||||
if (!markdownFile) {
|
||||
console.log(` ⚠ No markdown file found for ${doc.slug}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(DOCS_DIR, markdownFile);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(` ⚠ File not found: ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read markdown content
|
||||
const markdown = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Parse sections
|
||||
const sections = parseDocumentSections(markdown, doc.content_html);
|
||||
|
||||
// Add HTML content to each section
|
||||
const sectionsWithHtml = sections.map(section => ({
|
||||
...section,
|
||||
content_html: markdownToHtml(section.content)
|
||||
}));
|
||||
|
||||
console.log(` ✓ Parsed ${sections.length} sections`);
|
||||
|
||||
// Category breakdown
|
||||
const categoryCount = {};
|
||||
sectionsWithHtml.forEach(s => {
|
||||
categoryCount[s.category] = (categoryCount[s.category] || 0) + 1;
|
||||
});
|
||||
console.log(` Categories:`, categoryCount);
|
||||
|
||||
// Technical level breakdown
|
||||
const levelCount = {};
|
||||
sectionsWithHtml.forEach(s => {
|
||||
levelCount[s.technicalLevel] = (levelCount[s.technicalLevel] || 0) + 1;
|
||||
});
|
||||
console.log(` Levels:`, levelCount);
|
||||
|
||||
// Update document with sections
|
||||
await collection.updateOne(
|
||||
{ _id: doc._id },
|
||||
{
|
||||
$set: {
|
||||
sections: sectionsWithHtml,
|
||||
'metadata.sections_count': sections.length,
|
||||
'metadata.last_section_update': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` ✓ Updated document\n`);
|
||||
}
|
||||
|
||||
console.log('=== Section Addition Complete ===');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Instruction History Analytics
|
||||
*
|
||||
* Analyzes instruction-history.json to provide insights into:
|
||||
* - Instruction usage patterns
|
||||
* - Most violated instructions
|
||||
* - Instructions never referenced
|
||||
* - Quadrant distribution
|
||||
* - Enforcement effectiveness
|
||||
*
|
||||
* Usage: node scripts/analyze-instruction-violations.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
const INCIDENTS_PATH = path.join(__dirname, '../.claude/framework-incidents.json');
|
||||
|
||||
/**
|
||||
* Color output helpers
|
||||
*/
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
magenta: '\x1b[35m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function header(message) {
|
||||
console.log('');
|
||||
log('═'.repeat(80), 'cyan');
|
||||
log(` ${message}`, 'bright');
|
||||
log('═'.repeat(80), 'cyan');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function section(message) {
|
||||
console.log('');
|
||||
log(`▶ ${message}`, 'blue');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load instruction history
|
||||
*/
|
||||
function loadInstructions() {
|
||||
try {
|
||||
const data = fs.readFileSync(INSTRUCTION_HISTORY_PATH, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (err) {
|
||||
log(`Error loading instruction history: ${err.message}`, 'red');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load framework incidents (if exists)
|
||||
*/
|
||||
function loadIncidents() {
|
||||
try {
|
||||
if (!fs.existsSync(INCIDENTS_PATH)) {
|
||||
return { incidents: [] };
|
||||
}
|
||||
const data = fs.readFileSync(INCIDENTS_PATH, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch (err) {
|
||||
log(`Warning: Could not load incidents database: ${err.message}`, 'yellow');
|
||||
return { incidents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze instruction usage
|
||||
*/
|
||||
function analyzeInstructions(history) {
|
||||
const instructions = history.instructions || [];
|
||||
const active = instructions.filter(i => i.active);
|
||||
const inactive = instructions.filter(i => !i.active);
|
||||
|
||||
// Quadrant distribution
|
||||
const byQuadrant = {
|
||||
STRATEGIC: active.filter(i => i.quadrant === 'STRATEGIC').length,
|
||||
OPERATIONAL: active.filter(i => i.quadrant === 'OPERATIONAL').length,
|
||||
TACTICAL: active.filter(i => i.quadrant === 'TACTICAL').length,
|
||||
SYSTEM: active.filter(i => i.quadrant === 'SYSTEM').length,
|
||||
STOCHASTIC: active.filter(i => i.quadrant === 'STOCHASTIC').length
|
||||
};
|
||||
|
||||
// Persistence distribution
|
||||
const byPersistence = {
|
||||
HIGH: active.filter(i => i.persistence === 'HIGH').length,
|
||||
MEDIUM: active.filter(i => i.persistence === 'MEDIUM').length,
|
||||
LOW: active.filter(i => i.persistence === 'LOW').length,
|
||||
VARIABLE: active.filter(i => i.persistence === 'VARIABLE').length
|
||||
};
|
||||
|
||||
// Temporal scope distribution
|
||||
const byScope = {
|
||||
PERMANENT: active.filter(i => i.temporal_scope === 'PERMANENT').length,
|
||||
PROJECT: active.filter(i => i.temporal_scope === 'PROJECT').length,
|
||||
PHASE: active.filter(i => i.temporal_scope === 'PHASE').length,
|
||||
SESSION: active.filter(i => i.temporal_scope === 'SESSION').length
|
||||
};
|
||||
|
||||
return {
|
||||
total: instructions.length,
|
||||
active: active.length,
|
||||
inactive: inactive.length,
|
||||
byQuadrant,
|
||||
byPersistence,
|
||||
byScope,
|
||||
instructions: active
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze violations from incidents database
|
||||
*/
|
||||
function analyzeViolations(incidents) {
|
||||
const violations = {};
|
||||
|
||||
incidents.forEach(incident => {
|
||||
const instId = incident.instruction_violated || incident.id;
|
||||
if (!violations[instId]) {
|
||||
violations[instId] = {
|
||||
count: 0,
|
||||
tokens_wasted: 0,
|
||||
incidents: []
|
||||
};
|
||||
}
|
||||
violations[instId].count++;
|
||||
violations[instId].tokens_wasted += incident.tokens_wasted || 0;
|
||||
violations[instId].incidents.push(incident);
|
||||
});
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find never-referenced instructions
|
||||
*/
|
||||
function findUnusedInstructions(instructions, violations) {
|
||||
const neverViolated = instructions.filter(i => !violations[i.id]);
|
||||
|
||||
return {
|
||||
neverViolated: neverViolated.map(i => i.id),
|
||||
strategicNeverViolated: neverViolated.filter(i => i.quadrant === 'STRATEGIC').map(i => i.id)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate enforcement effectiveness
|
||||
*/
|
||||
function calculateEnforcement(instructions, violations) {
|
||||
// Hook-enforced instructions (those with architectural enforcement)
|
||||
const hookEnforced = instructions.filter(i =>
|
||||
i.enforcement === 'architectural' ||
|
||||
i.enforcement === 'hook' ||
|
||||
i.notes?.includes('hook') ||
|
||||
i.notes?.includes('architectural')
|
||||
);
|
||||
|
||||
const voluntary = instructions.filter(i =>
|
||||
!hookEnforced.includes(i)
|
||||
);
|
||||
|
||||
// Calculate violation rates
|
||||
const hookEnforcedViolations = hookEnforced.filter(i => violations[i.id]);
|
||||
const voluntaryViolations = voluntary.filter(i => violations[i.id]);
|
||||
|
||||
const hookEnforcementRate = hookEnforced.length > 0
|
||||
? ((hookEnforced.length - hookEnforcedViolations.length) / hookEnforced.length * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
const voluntaryComplianceRate = voluntary.length > 0
|
||||
? ((voluntary.length - voluntaryViolations.length) / voluntary.length * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
hookEnforced: hookEnforced.length,
|
||||
voluntary: voluntary.length,
|
||||
hookEnforcementRate,
|
||||
voluntaryComplianceRate,
|
||||
hookEnforcedViolations: hookEnforcedViolations.length,
|
||||
voluntaryViolations: voluntaryViolations.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main analytics
|
||||
*/
|
||||
function main() {
|
||||
header('Instruction History Analytics');
|
||||
|
||||
// Load data
|
||||
const history = loadInstructions();
|
||||
const incidentsData = loadIncidents();
|
||||
const incidents = incidentsData.incidents || [];
|
||||
|
||||
log(`📊 Analyzing ${history.instructions?.length || 0} instructions and ${incidents.length} incidents`, 'cyan');
|
||||
|
||||
// Analyze
|
||||
const analysis = analyzeInstructions(history);
|
||||
const violations = analyzeViolations(incidents);
|
||||
const unused = findUnusedInstructions(analysis.instructions, violations);
|
||||
const enforcement = calculateEnforcement(analysis.instructions, violations);
|
||||
|
||||
// Display results
|
||||
|
||||
// 1. Overview
|
||||
section('1. Instruction Overview');
|
||||
log(` Total instructions: ${analysis.total}`, 'cyan');
|
||||
log(` Active: ${analysis.active}`, 'green');
|
||||
log(` Inactive: ${analysis.inactive}`, 'yellow');
|
||||
|
||||
// 2. Distribution by Quadrant
|
||||
section('2. Distribution by Quadrant');
|
||||
Object.entries(analysis.byQuadrant).forEach(([quadrant, count]) => {
|
||||
const bar = '█'.repeat(Math.ceil(count / 2));
|
||||
log(` ${quadrant.padEnd(12)}: ${count.toString().padStart(2)} ${bar}`, 'cyan');
|
||||
});
|
||||
|
||||
// 3. Distribution by Persistence
|
||||
section('3. Distribution by Persistence');
|
||||
Object.entries(analysis.byPersistence).forEach(([level, count]) => {
|
||||
const bar = '█'.repeat(Math.ceil(count / 2));
|
||||
log(` ${level.padEnd(12)}: ${count.toString().padStart(2)} ${bar}`, 'cyan');
|
||||
});
|
||||
|
||||
// 4. Distribution by Temporal Scope
|
||||
section('4. Distribution by Temporal Scope');
|
||||
Object.entries(analysis.byScope).forEach(([scope, count]) => {
|
||||
const bar = '█'.repeat(Math.ceil(count / 2));
|
||||
log(` ${scope.padEnd(12)}: ${count.toString().padStart(2)} ${bar}`, 'cyan');
|
||||
});
|
||||
|
||||
// 5. Most Violated Instructions
|
||||
section('5. Most Violated Instructions');
|
||||
const violationList = Object.entries(violations)
|
||||
.sort((a, b) => b[1].count - a[1].count);
|
||||
|
||||
if (violationList.length === 0) {
|
||||
log(' ✅ No violations recorded', 'green');
|
||||
} else {
|
||||
violationList.slice(0, 10).forEach(([instId, data]) => {
|
||||
const instruction = analysis.instructions.find(i => i.id === instId);
|
||||
const text = instruction ? instruction.text.substring(0, 60) + '...' : 'Unknown instruction';
|
||||
log(` ${instId}: ${data.count} violation(s), ${data.tokens_wasted.toLocaleString()} tokens wasted`, 'red');
|
||||
log(` "${text}"`, 'yellow');
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Never Violated Instructions
|
||||
section('6. Never Violated Instructions');
|
||||
if (unused.neverViolated.length === 0) {
|
||||
log(' ⚠️ All instructions have been violated at least once!', 'yellow');
|
||||
} else {
|
||||
log(` ${unused.neverViolated.length} instructions with 100% compliance:`, 'green');
|
||||
unused.neverViolated.slice(0, 10).forEach(instId => {
|
||||
const instruction = analysis.instructions.find(i => i.id === instId);
|
||||
if (instruction) {
|
||||
log(` ${instId}: ${instruction.text.substring(0, 70)}`, 'cyan');
|
||||
}
|
||||
});
|
||||
if (unused.neverViolated.length > 10) {
|
||||
log(` ... and ${unused.neverViolated.length - 10} more`, 'cyan');
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Enforcement Effectiveness
|
||||
section('7. Enforcement Effectiveness');
|
||||
log(` Hook-enforced instructions: ${enforcement.hookEnforced}`, 'cyan');
|
||||
log(` Violations: ${enforcement.hookEnforcedViolations}`, enforcement.hookEnforcedViolations > 0 ? 'red' : 'green');
|
||||
log(` Compliance rate: ${enforcement.hookEnforcementRate}%`, enforcement.hookEnforcementRate >= 95 ? 'green' : 'yellow');
|
||||
|
||||
console.log('');
|
||||
log(` Voluntary compliance instructions: ${enforcement.voluntary}`, 'cyan');
|
||||
log(` Violations: ${enforcement.voluntaryViolations}`, enforcement.voluntaryViolations > 0 ? 'red' : 'green');
|
||||
log(` Compliance rate: ${enforcement.voluntaryComplianceRate}%`, enforcement.voluntaryComplianceRate >= 95 ? 'green' : 'yellow');
|
||||
|
||||
console.log('');
|
||||
if (enforcement.hookEnforcementRate > enforcement.voluntaryComplianceRate) {
|
||||
log(` ✅ Hook enforcement is ${(enforcement.hookEnforcementRate - enforcement.voluntaryComplianceRate).toFixed(1)}% more effective`, 'green');
|
||||
log(` 💡 Recommendation: Convert more voluntary compliance to architectural enforcement`, 'cyan');
|
||||
} else if (enforcement.voluntaryComplianceRate >= 95) {
|
||||
log(` ✅ Voluntary compliance is working well (${enforcement.voluntaryComplianceRate}%)`, 'green');
|
||||
} else {
|
||||
log(` ⚠️ Consider improving enforcement mechanisms`, 'yellow');
|
||||
}
|
||||
|
||||
// 8. Recommendations
|
||||
section('8. Recommendations');
|
||||
|
||||
const recommendations = [];
|
||||
|
||||
// High-violation instructions needing enforcement
|
||||
const highViolation = violationList.filter(([_, data]) => data.count >= 2);
|
||||
if (highViolation.length > 0) {
|
||||
recommendations.push({
|
||||
priority: 'HIGH',
|
||||
text: `${highViolation.length} instruction(s) violated 2+ times - add architectural enforcement`,
|
||||
details: highViolation.map(([id]) => id)
|
||||
});
|
||||
}
|
||||
|
||||
// Strategic instructions that are never violated (good!)
|
||||
if (unused.strategicNeverViolated.length > 0) {
|
||||
recommendations.push({
|
||||
priority: 'INFO',
|
||||
text: `${unused.strategicNeverViolated.length} STRATEGIC instructions have 100% compliance - document success`,
|
||||
details: unused.strategicNeverViolated.slice(0, 5)
|
||||
});
|
||||
}
|
||||
|
||||
// Voluntary compliance gaps
|
||||
if (enforcement.voluntaryComplianceRate < 80) {
|
||||
recommendations.push({
|
||||
priority: 'MEDIUM',
|
||||
text: `Voluntary compliance at ${enforcement.voluntaryComplianceRate}% - convert to hooks or improve documentation`,
|
||||
details: []
|
||||
});
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
log(' ✅ No recommendations - framework is performing well!', 'green');
|
||||
} else {
|
||||
recommendations.forEach((rec, i) => {
|
||||
const color = rec.priority === 'HIGH' ? 'red' : rec.priority === 'MEDIUM' ? 'yellow' : 'cyan';
|
||||
log(` ${i + 1}. [${rec.priority}] ${rec.text}`, color);
|
||||
if (rec.details.length > 0) {
|
||||
rec.details.forEach(detail => {
|
||||
log(` - ${detail}`, 'cyan');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Summary footer
|
||||
header('Analytics Complete');
|
||||
console.log('');
|
||||
log(` 📈 Key Metrics:`, 'bright');
|
||||
log(` Active Instructions: ${analysis.active}`, 'cyan');
|
||||
log(` Recorded Violations: ${incidents.length}`, incidents.length > 0 ? 'yellow' : 'green');
|
||||
log(` Tokens Wasted: ${Object.values(violations).reduce((sum, v) => sum + v.tokens_wasted, 0).toLocaleString()}`, 'red');
|
||||
log(` Hook Enforcement Rate: ${enforcement.hookEnforcementRate}%`, 'green');
|
||||
log(` Voluntary Compliance Rate: ${enforcement.voluntaryComplianceRate}%`, 'yellow');
|
||||
console.log('');
|
||||
log(` 💡 Next Steps:`, 'bright');
|
||||
log(` - Review high-violation instructions for enforcement gaps`, 'cyan');
|
||||
log(` - Document successful compliance patterns`, 'cyan');
|
||||
log(` - Convert voluntary → architectural where violations occur`, 'cyan');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Run
|
||||
main();
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
/**
|
||||
* Archive All Internal Documents
|
||||
* Mass archive of project tracking, internal, and confidential documents
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
// MongoDB connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
// Documents to mark as confidential (not archived, just hidden)
|
||||
const CONFIDENTIAL_DOCS = [
|
||||
'security-audit-report',
|
||||
'koha-stripe-payment-setup-guide',
|
||||
'koha-production-deployment-guide',
|
||||
'appendix-e-contact-information',
|
||||
'cover-letter-for-anthropic-submission'
|
||||
];
|
||||
|
||||
// Documents to archive (project tracking, internal planning)
|
||||
const ARCHIVE_PATTERNS = [
|
||||
'phase-2-', // All Phase 2 planning docs
|
||||
'session-handoff', // All session handoffs
|
||||
'appendix-b-', // Duplicate case studies
|
||||
'implementation-roadmap', // Outdated planning
|
||||
'implementation-guide-python', // Outdated code examples
|
||||
'research-foundations-scholarly', // Academic, not practical
|
||||
'tractatus-blog-post-outlines', // Internal planning
|
||||
'tractatus-project-implementation-progress-report', // Project tracking
|
||||
'tractatus-production-comprehensive-testing-checklist', // Internal testing
|
||||
'tractatus-production-testing-results', // Test results
|
||||
'tractatus-governance-framework-test-suite', // Internal testing
|
||||
'ai-features-implementation-session' // Session tracking
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('=== Archiving All Internal Documents ===\n');
|
||||
|
||||
let client;
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
console.log('Connecting to MongoDB...');
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
console.log('✓ Connected\n');
|
||||
|
||||
// 1. Mark confidential documents
|
||||
console.log('=== Marking Confidential Documents ===\n');
|
||||
let confidentialCount = 0;
|
||||
|
||||
for (const slug of CONFIDENTIAL_DOCS) {
|
||||
const result = await collection.updateOne(
|
||||
{ slug },
|
||||
{
|
||||
$set: {
|
||||
visibility: 'confidential',
|
||||
category: 'internal',
|
||||
order: 999
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount > 0) {
|
||||
console.log(`✓ Marked confidential: ${slug}`);
|
||||
confidentialCount++;
|
||||
}
|
||||
}
|
||||
console.log(`\n✓ Total confidential: ${confidentialCount}\n`);
|
||||
|
||||
// 2. Archive documents matching patterns
|
||||
console.log('=== Archiving Project Tracking Documents ===\n');
|
||||
let archivedCount = 0;
|
||||
|
||||
const allDocs = await collection.find({ visibility: { $ne: 'confidential' } }).toArray();
|
||||
|
||||
for (const doc of allDocs) {
|
||||
const shouldArchive = ARCHIVE_PATTERNS.some(pattern =>
|
||||
doc.slug.includes(pattern)
|
||||
);
|
||||
|
||||
if (shouldArchive && doc.visibility !== 'archived') {
|
||||
const result = await collection.updateOne(
|
||||
{ _id: doc._id },
|
||||
{
|
||||
$set: {
|
||||
visibility: 'archived',
|
||||
category: 'project-tracking',
|
||||
order: 999,
|
||||
archiveNote: 'Internal project tracking document. Not relevant for public documentation.'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount > 0) {
|
||||
console.log(`✓ Archived: ${doc.slug}`);
|
||||
archivedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`\n✓ Total archived: ${archivedCount}\n`);
|
||||
|
||||
// 3. Handle recent case studies (keep but categorize)
|
||||
console.log('=== Categorizing Case Studies ===\n');
|
||||
const caseStudySlugs = [
|
||||
'our-framework-in-action-detecting-and-correcting-ai-fabrications',
|
||||
'framework-governance-in-action-pre-publication-security-audit',
|
||||
'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery',
|
||||
'when-frameworks-fail-and-why-thats-ok'
|
||||
];
|
||||
|
||||
let caseStudyCount = 0;
|
||||
for (const [index, slug] of caseStudySlugs.entries()) {
|
||||
const result = await collection.updateOne(
|
||||
{ slug },
|
||||
{
|
||||
$set: {
|
||||
visibility: 'public',
|
||||
category: 'practical',
|
||||
audience: 'general',
|
||||
order: 8 + index // Orders 8-11
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount > 0) {
|
||||
console.log(`✓ Categorized: ${slug} (order: ${8 + index})`);
|
||||
caseStudyCount++;
|
||||
}
|
||||
}
|
||||
console.log(`\n✓ Total case studies: ${caseStudyCount}\n`);
|
||||
|
||||
// Summary
|
||||
console.log('=== Final Summary ===\n');
|
||||
const publicCount = await collection.countDocuments({ visibility: 'public' });
|
||||
const archivedTotal = await collection.countDocuments({ visibility: 'archived' });
|
||||
const confidentialTotal = await collection.countDocuments({ visibility: 'confidential' });
|
||||
|
||||
console.log(`📖 Public documents: ${publicCount}`);
|
||||
console.log(`📦 Archived documents: ${archivedTotal}`);
|
||||
console.log(`🔒 Confidential documents: ${confidentialTotal}`);
|
||||
console.log(`\nTotal: ${publicCount + archivedTotal + confidentialTotal} documents\n`);
|
||||
|
||||
// Show public documents
|
||||
console.log('=== Public Documents (Final List) ===\n');
|
||||
const publicDocs = await collection.find({ visibility: 'public' })
|
||||
.sort({ order: 1 })
|
||||
.project({ title: 1, order: 1, category: 1 })
|
||||
.toArray();
|
||||
|
||||
publicDocs.forEach(doc => {
|
||||
console.log(` ${doc.order}. ${doc.title} [${doc.category}]`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
/**
|
||||
* Archive Outdated Documents
|
||||
* Sets visibility: 'archived' for 10 documents identified in audit
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
// MongoDB connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
// Documents to archive with reasons
|
||||
const DOCUMENTS_TO_ARCHIVE = [
|
||||
{
|
||||
slug: 'introduction-to-the-tractatus-framework',
|
||||
category: 'archived',
|
||||
archiveNote: 'Superseded by Architectural Overview & Research Status. References outdated filesystem-only architecture.',
|
||||
reason: 'Outdated architecture (pre-MongoDB)'
|
||||
},
|
||||
{
|
||||
slug: 'tractatus-based-llm-architecture-for-ai-safety',
|
||||
category: 'archived',
|
||||
archiveNote: 'Historical architecture proposal. See Architectural Overview for implemented architecture.',
|
||||
reason: 'Pre-Phase 5 architecture proposal'
|
||||
},
|
||||
{
|
||||
slug: 'executive-brief-tractatus-based-llm-architecture-for-ai-safety',
|
||||
category: 'archived',
|
||||
archiveNote: 'Historical brief based on pre-Phase 5 architecture. See Architectural Overview for current status.',
|
||||
reason: 'Pre-Phase 5 executive brief'
|
||||
},
|
||||
{
|
||||
slug: 'tractatus-framework-enforcement-for-claude-code',
|
||||
category: 'archived',
|
||||
archiveNote: 'Development tool documentation. See Implementation Guide for production deployment.',
|
||||
reason: 'Internal development tool'
|
||||
},
|
||||
{
|
||||
slug: 'organizational-theory-foundations-of-the-tractatus-framework',
|
||||
category: 'archived',
|
||||
archiveNote: 'Academic foundations. See Core Concepts for practical overview.',
|
||||
reason: 'Academic content, not practical'
|
||||
},
|
||||
{
|
||||
slug: 'phase-5-poc-session-1-summary',
|
||||
category: 'project-tracking',
|
||||
archiveNote: 'Project tracking - Phase 5 Session 1. See Architectural Overview for complete project history.',
|
||||
reason: 'Project tracking'
|
||||
},
|
||||
{
|
||||
slug: 'phase-5-poc-session-2-summary',
|
||||
category: 'project-tracking',
|
||||
archiveNote: 'Project tracking - Phase 5 Session 2. See Architectural Overview for complete project history.',
|
||||
reason: 'Project tracking'
|
||||
},
|
||||
{
|
||||
slug: 'research-scope-feasibility-of-llm-integrated-tractatus-framework',
|
||||
category: 'research-proposal',
|
||||
archiveNote: 'Research proposal (not completed work). See Architectural Overview for actual implementation status.',
|
||||
reason: 'Research proposal'
|
||||
},
|
||||
{
|
||||
slug: 'research-topic-concurrent-session-architecture-limitations-in-claude-code-governance',
|
||||
category: 'research-topic',
|
||||
archiveNote: 'Open research question. See Architectural Overview for current architecture limitations.',
|
||||
reason: 'Open research question'
|
||||
},
|
||||
{
|
||||
slug: 'research-topic-rule-proliferation-and-transactional-overhead-in-ai-governance',
|
||||
category: 'research-topic',
|
||||
archiveNote: 'Open research question. See Architectural Overview for current governance approach.',
|
||||
reason: 'Open research question'
|
||||
}
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('=== Archiving Outdated Documents ===\n');
|
||||
|
||||
let client;
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
console.log('Connecting to MongoDB...');
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
console.log('✓ Connected\n');
|
||||
|
||||
let archived = 0;
|
||||
let notFound = 0;
|
||||
|
||||
// Archive each document
|
||||
for (const doc of DOCUMENTS_TO_ARCHIVE) {
|
||||
console.log(`Archiving: ${doc.slug}`);
|
||||
console.log(` Reason: ${doc.reason}`);
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ slug: doc.slug },
|
||||
{
|
||||
$set: {
|
||||
visibility: 'archived',
|
||||
category: doc.category,
|
||||
archiveNote: doc.archiveNote,
|
||||
order: 999
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount > 0) {
|
||||
console.log(` ✓ Archived\n`);
|
||||
archived++;
|
||||
} else {
|
||||
console.log(` ⚠ Not found in database\n`);
|
||||
notFound++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('=== Summary ===\n');
|
||||
console.log(`✓ Archived: ${archived} documents`);
|
||||
if (notFound > 0) {
|
||||
console.log(`⚠ Not found: ${notFound} documents`);
|
||||
}
|
||||
console.log(`\nTotal processed: ${DOCUMENTS_TO_ARCHIVE.length}`);
|
||||
|
||||
// Verify archives
|
||||
console.log('\n=== Verification ===\n');
|
||||
const archivedCount = await collection.countDocuments({ visibility: 'archived' });
|
||||
const publicCount = await collection.countDocuments({ visibility: 'public' });
|
||||
console.log(`Archived documents: ${archivedCount}`);
|
||||
console.log(`Public documents: ${publicCount}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Accessibility Audit Script
|
||||
*
|
||||
* Runs automated accessibility checks on all main pages
|
||||
* using pa11y (WCAG 2.1 AA standard)
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const pa11y = require('pa11y');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function section(message) {
|
||||
console.log('');
|
||||
log(`▶ ${message}`, 'cyan');
|
||||
}
|
||||
|
||||
function success(message) {
|
||||
log(` ✓ ${message}`, 'green');
|
||||
}
|
||||
|
||||
function warning(message) {
|
||||
log(` ⚠ ${message}`, 'yellow');
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
log(` ✗ ${message}`, 'red');
|
||||
}
|
||||
|
||||
// Pages to audit
|
||||
const pages = [
|
||||
{ name: 'Homepage', url: 'http://localhost:9000/' },
|
||||
{ name: 'Researcher', url: 'http://localhost:9000/researcher.html' },
|
||||
{ name: 'Implementer', url: 'http://localhost:9000/implementer.html' },
|
||||
{ name: 'Leader', url: 'http://localhost:9000/leader.html' },
|
||||
{ name: 'About', url: 'http://localhost:9000/about.html' },
|
||||
{ name: 'Values', url: 'http://localhost:9000/about/values.html' },
|
||||
{ name: 'Media Inquiry', url: 'http://localhost:9000/media-inquiry.html' },
|
||||
{ name: 'Case Submission', url: 'http://localhost:9000/case-submission.html' },
|
||||
{ name: 'Docs', url: 'http://localhost:9000/docs.html' }
|
||||
];
|
||||
|
||||
// pa11y configuration
|
||||
const pa11yConfig = {
|
||||
standard: 'WCAG2AA',
|
||||
timeout: 30000,
|
||||
wait: 1000,
|
||||
chromeLaunchConfig: {
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
},
|
||||
// Common issues to ignore (if needed)
|
||||
ignore: []
|
||||
};
|
||||
|
||||
async function auditPage(page) {
|
||||
try {
|
||||
const results = await pa11y(page.url, pa11yConfig);
|
||||
|
||||
return {
|
||||
name: page.name,
|
||||
url: page.url,
|
||||
issues: results.issues,
|
||||
error: false
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
name: page.name,
|
||||
url: page.url,
|
||||
error: true,
|
||||
errorMessage: err.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function categorizeIssues(issues) {
|
||||
const categorized = {
|
||||
error: [],
|
||||
warning: [],
|
||||
notice: []
|
||||
};
|
||||
|
||||
issues.forEach(issue => {
|
||||
categorized[issue.type].push(issue);
|
||||
});
|
||||
|
||||
return categorized;
|
||||
}
|
||||
|
||||
function printIssue(issue, index) {
|
||||
const typeColor = {
|
||||
error: 'red',
|
||||
warning: 'yellow',
|
||||
notice: 'cyan'
|
||||
};
|
||||
|
||||
console.log('');
|
||||
log(` ${index + 1}. [${issue.type.toUpperCase()}] ${issue.message}`, typeColor[issue.type]);
|
||||
log(` Code: ${issue.code}`, 'reset');
|
||||
log(` Element: ${issue.context.substring(0, 100)}${issue.context.length > 100 ? '...' : ''}`, 'reset');
|
||||
log(` Selector: ${issue.selector}`, 'reset');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Tractatus Accessibility Audit (WCAG 2.1 AA)', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
const allResults = [];
|
||||
let totalErrors = 0;
|
||||
let totalWarnings = 0;
|
||||
let totalNotices = 0;
|
||||
|
||||
for (const page of pages) {
|
||||
section(`Auditing: ${page.name}`);
|
||||
const result = await auditPage(page);
|
||||
allResults.push(result);
|
||||
|
||||
if (result.error) {
|
||||
error(`Failed to audit: ${result.errorMessage}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const categorized = categorizeIssues(result.issues);
|
||||
|
||||
const errorCount = categorized.error.length;
|
||||
const warningCount = categorized.warning.length;
|
||||
const noticeCount = categorized.notice.length;
|
||||
|
||||
totalErrors += errorCount;
|
||||
totalWarnings += warningCount;
|
||||
totalNotices += noticeCount;
|
||||
|
||||
if (errorCount === 0 && warningCount === 0 && noticeCount === 0) {
|
||||
success(`No accessibility issues found!`);
|
||||
} else {
|
||||
if (errorCount > 0) error(`${errorCount} errors`);
|
||||
if (warningCount > 0) warning(`${warningCount} warnings`);
|
||||
if (noticeCount > 0) log(` ℹ ${noticeCount} notices`, 'cyan');
|
||||
|
||||
// Print first 3 errors/warnings
|
||||
const criticalIssues = [...categorized.error, ...categorized.warning].slice(0, 3);
|
||||
if (criticalIssues.length > 0) {
|
||||
log(' Top issues:', 'bright');
|
||||
criticalIssues.forEach((issue, idx) => {
|
||||
printIssue(issue, idx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Summary', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
log(` Pages Audited: ${pages.length}`, 'bright');
|
||||
log(` Total Errors: ${totalErrors}`, totalErrors > 0 ? 'red' : 'green');
|
||||
log(` Total Warnings: ${totalWarnings}`, totalWarnings > 0 ? 'yellow' : 'green');
|
||||
log(` Total Notices: ${totalNotices}`, 'cyan');
|
||||
console.log('');
|
||||
|
||||
// Save detailed report
|
||||
const reportPath = path.join(__dirname, '../audit-reports/accessibility-report.json');
|
||||
const reportDir = path.dirname(reportPath);
|
||||
|
||||
if (!fs.existsSync(reportDir)) {
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(reportPath, JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
standard: 'WCAG 2.1 AA',
|
||||
summary: {
|
||||
pagesAudited: pages.length,
|
||||
totalErrors,
|
||||
totalWarnings,
|
||||
totalNotices
|
||||
},
|
||||
results: allResults
|
||||
}, null, 2));
|
||||
|
||||
success(`Detailed report saved: ${reportPath}`);
|
||||
console.log('');
|
||||
|
||||
// Exit code based on errors
|
||||
if (totalErrors > 0) {
|
||||
error('Accessibility audit FAILED - errors found');
|
||||
process.exit(1);
|
||||
} else if (totalWarnings > 0) {
|
||||
warning('Accessibility audit PASSED with warnings');
|
||||
process.exit(0);
|
||||
} else {
|
||||
success('Accessibility audit PASSED');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('');
|
||||
error(`Audit failed: ${err.message}`);
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
const { MongoClient } = require('mongodb');
|
||||
|
||||
// The 34 public documents by slug
|
||||
const PUBLIC_SLUGS = [
|
||||
// Getting Started
|
||||
'introduction',
|
||||
'architectural-safeguards-against-llm-hierarchical-dominance-prose',
|
||||
'core-concepts',
|
||||
'tractatus-ai-safety-framework-core-values-and-principles',
|
||||
|
||||
// Technical Reference
|
||||
'technical-architecture',
|
||||
'implementation-guide',
|
||||
'implementation-roadmap-24-month-deployment-plan',
|
||||
'GLOSSARY',
|
||||
'comparison-matrix',
|
||||
'implementation-guide-v1.1',
|
||||
'api-reference-complete',
|
||||
'api-javascript-examples',
|
||||
'api-python-examples',
|
||||
'openapi-specification',
|
||||
|
||||
// Theory & Research
|
||||
'executive-summary-tractatus-inflection-point',
|
||||
'architectural-overview-and-research-status',
|
||||
'organizational-theory-foundations',
|
||||
'pluralistic-values-research-foundations',
|
||||
|
||||
// Advanced Topics
|
||||
'value-pluralism-faq',
|
||||
'pluralistic-values-deliberation-plan-v2',
|
||||
|
||||
// Case Studies
|
||||
'the-27027-incident-a-case-study-in-pattern-recognition-bias',
|
||||
'when-frameworks-fail-and-why-thats-ok',
|
||||
'our-framework-in-action-detecting-and-correcting-ai-fabrications',
|
||||
'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery',
|
||||
'case-studies-real-world-llm-failure-modes',
|
||||
|
||||
// Business & Leadership
|
||||
'business-case-tractatus-framework',
|
||||
|
||||
// Archives
|
||||
'llm-integration-feasibility-research-scope',
|
||||
'case-studies-real-world-llm-failure-modes-appendix',
|
||||
'implementation-guide-python-examples',
|
||||
'tractatus-framework-enforcement-claude-code',
|
||||
'research-topic-concurrent-session-architecture',
|
||||
'research-topic-rule-proliferation-transactional-overhead',
|
||||
'phase-5-poc-session-1-summary',
|
||||
'phase-5-poc-session-2-summary'
|
||||
];
|
||||
|
||||
async function checkCardViewStatus() {
|
||||
const client = new MongoClient('mongodb://localhost:27017');
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db('tractatus_dev');
|
||||
const collection = db.collection('documents');
|
||||
|
||||
console.log(`\n=== CHECKING CARD VIEW STATUS FOR 34 PUBLIC DOCUMENTS ===\n`);
|
||||
|
||||
const documents = await collection.find({
|
||||
slug: { $in: PUBLIC_SLUGS }
|
||||
}).toArray();
|
||||
|
||||
console.log(`Found ${documents.length} / ${PUBLIC_SLUGS.length} documents in database\n`);
|
||||
|
||||
const withCards = [];
|
||||
const withoutCards = [];
|
||||
const notFound = [];
|
||||
|
||||
PUBLIC_SLUGS.forEach(slug => {
|
||||
const doc = documents.find(d => d.slug === slug);
|
||||
if (!doc) {
|
||||
notFound.push(slug);
|
||||
} else if (doc.sections && doc.sections.length > 0) {
|
||||
withCards.push({
|
||||
slug: doc.slug,
|
||||
title: doc.title,
|
||||
sections: doc.sections.length,
|
||||
category: doc.category || 'none',
|
||||
order: doc.order || 999
|
||||
});
|
||||
} else {
|
||||
withoutCards.push({
|
||||
slug: doc.slug,
|
||||
title: doc.title,
|
||||
category: doc.category || 'none',
|
||||
order: doc.order || 999
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ WITH CARD VIEW (${withCards.length} docs):`);
|
||||
withCards.forEach(doc => {
|
||||
console.log(` [order:${doc.order}] ${doc.title}`);
|
||||
console.log(` slug: ${doc.slug} | sections: ${doc.sections} | category: ${doc.category}`);
|
||||
});
|
||||
|
||||
console.log(`\n❌ WITHOUT CARD VIEW (${withoutCards.length} docs):`);
|
||||
withoutCards.forEach(doc => {
|
||||
console.log(` [order:${doc.order}] ${doc.title}`);
|
||||
console.log(` slug: ${doc.slug} | category: ${doc.category}`);
|
||||
});
|
||||
|
||||
if (notFound.length > 0) {
|
||||
console.log(`\n⚠️ NOT FOUND IN DATABASE (${notFound.length} slugs):`);
|
||||
notFound.forEach(slug => console.log(` ${slug}`));
|
||||
}
|
||||
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
checkCardViewStatus().catch(console.error);
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Color Contrast Checker
|
||||
*
|
||||
* Verifies color contrast ratios meet WCAG 2.1 AA standards (4.5:1 normal text, 3:1 large text)
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function success(message) {
|
||||
log(` ✓ ${message}`, 'green');
|
||||
}
|
||||
|
||||
function warning(message) {
|
||||
log(` ⚠ ${message}`, 'yellow');
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
log(` ✗ ${message}`, 'red');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color to RGB
|
||||
*/
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate relative luminance (WCAG formula)
|
||||
*/
|
||||
function getLuminance(rgb) {
|
||||
const rsRGB = rgb.r / 255;
|
||||
const gsRGB = rgb.g / 255;
|
||||
const bsRGB = rgb.b / 255;
|
||||
|
||||
const r = rsRGB <= 0.03928 ? rsRGB / 12.92 : Math.pow((rsRGB + 0.055) / 1.055, 2.4);
|
||||
const g = gsRGB <= 0.03928 ? gsRGB / 12.92 : Math.pow((gsRGB + 0.055) / 1.055, 2.4);
|
||||
const b = bsRGB <= 0.03928 ? bsRGB / 12.92 : Math.pow((bsRGB + 0.055) / 1.055, 2.4);
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate contrast ratio between two colors
|
||||
*/
|
||||
function getContrastRatio(color1, color2) {
|
||||
const rgb1 = hexToRgb(color1);
|
||||
const rgb2 = hexToRgb(color2);
|
||||
|
||||
const lum1 = getLuminance(rgb1);
|
||||
const lum2 = getLuminance(rgb2);
|
||||
|
||||
const lighter = Math.max(lum1, lum2);
|
||||
const darker = Math.min(lum1, lum2);
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if contrast ratio meets WCAG AA standards
|
||||
*/
|
||||
function meetsWCAG_AA(ratio, largeText = false) {
|
||||
const threshold = largeText ? 3.0 : 4.5;
|
||||
return ratio >= threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tailwind color palette (common colors used in Tractatus site)
|
||||
*/
|
||||
const tailwindColors = {
|
||||
'white': '#ffffff',
|
||||
'gray-50': '#f9fafb',
|
||||
'gray-100': '#f3f4f6',
|
||||
'gray-200': '#e5e7eb',
|
||||
'gray-300': '#d1d5db',
|
||||
'gray-400': '#9ca3af',
|
||||
'gray-500': '#6b7280',
|
||||
'gray-600': '#4b5563',
|
||||
'gray-700': '#374151',
|
||||
'gray-800': '#1f2937',
|
||||
'gray-900': '#111827',
|
||||
'blue-50': '#eff6ff',
|
||||
'blue-100': '#dbeafe',
|
||||
'blue-400': '#60a5fa',
|
||||
'blue-500': '#3b82f6',
|
||||
'blue-600': '#2563eb',
|
||||
'blue-700': '#1d4ed8',
|
||||
'blue-800': '#1e40af',
|
||||
'blue-900': '#1e3a8a',
|
||||
'purple-500': '#a855f7',
|
||||
'purple-600': '#9333ea',
|
||||
'purple-700': '#7e22ce',
|
||||
'green-500': '#22c55e',
|
||||
'green-600': '#16a34a',
|
||||
'green-700': '#15803d',
|
||||
'yellow-600': '#ca8a04',
|
||||
'amber-500': '#f59e0b',
|
||||
'amber-800': '#92400e',
|
||||
'amber-900': '#78350f',
|
||||
'red-600': '#dc2626'
|
||||
};
|
||||
|
||||
/**
|
||||
* Color combinations used on site
|
||||
*/
|
||||
const colorCombinations = [
|
||||
// Body text on backgrounds
|
||||
{ name: 'Body text (gray-900 on white)', fg: 'gray-900', bg: 'white', largeText: false },
|
||||
{ name: 'Body text (gray-700 on white)', fg: 'gray-700', bg: 'white', largeText: false },
|
||||
{ name: 'Body text (gray-600 on white)', fg: 'gray-600', bg: 'white', largeText: false },
|
||||
{ name: 'Muted text (gray-500 on white)', fg: 'gray-500', bg: 'white', largeText: false },
|
||||
|
||||
// Links
|
||||
{ name: 'Link (blue-600 on white)', fg: 'blue-600', bg: 'white', largeText: false },
|
||||
{ name: 'Link hover (blue-700 on white)', fg: 'blue-700', bg: 'white', largeText: false },
|
||||
|
||||
// Buttons
|
||||
{ name: 'Button text (white on blue-600)', fg: 'white', bg: 'blue-600', largeText: false },
|
||||
{ name: 'Button hover (white on blue-700)', fg: 'white', bg: 'blue-700', largeText: false },
|
||||
{ name: 'Purple button (white on purple-600)', fg: 'white', bg: 'purple-600', largeText: false },
|
||||
{ name: 'Green button (white on green-700)', fg: 'white', bg: 'green-700', largeText: false },
|
||||
|
||||
// Hero section
|
||||
{ name: 'Hero subtitle (blue-100 on blue-700)', fg: 'blue-100', bg: 'blue-700', largeText: true },
|
||||
|
||||
// Footer
|
||||
{ name: 'Footer text (gray-400 on gray-900)', fg: 'gray-400', bg: 'gray-900', largeText: false },
|
||||
{ name: 'Footer links (blue-400 on gray-900)', fg: 'blue-400', bg: 'gray-900', largeText: false },
|
||||
|
||||
// Alerts/Messages
|
||||
{ name: 'Success message (green-900 on green-50)', fg: '#065f46', bg: '#d1fae5', largeText: false },
|
||||
{ name: 'Error message (red-900 on red-50)', fg: '#991b1b', bg: '#fee2e2', largeText: false },
|
||||
{ name: 'Warning message (amber-900 on amber-50)', fg: 'amber-900', bg: '#fef3c7', largeText: false },
|
||||
|
||||
// Cards/Sections
|
||||
{ name: 'Card text (gray-700 on white)', fg: 'gray-700', bg: 'white', largeText: false },
|
||||
{ name: 'Card header (gray-900 on white)', fg: 'gray-900', bg: 'white', largeText: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* Main check
|
||||
*/
|
||||
function main() {
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Color Contrast Checker (WCAG 2.1 AA)', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
let warnings = 0;
|
||||
|
||||
colorCombinations.forEach(combo => {
|
||||
const fgColor = tailwindColors[combo.fg] || combo.fg;
|
||||
const bgColor = tailwindColors[combo.bg] || combo.bg;
|
||||
|
||||
const ratio = getContrastRatio(fgColor, bgColor);
|
||||
const passes = meetsWCAG_AA(ratio, combo.largeText);
|
||||
const threshold = combo.largeText ? '3:1' : '4.5:1';
|
||||
|
||||
const ratioStr = ratio.toFixed(2) + ':1';
|
||||
|
||||
if (passes) {
|
||||
success(`${combo.name.padEnd(45)} ${ratioStr.padStart(8)} (>= ${threshold}) ✓`);
|
||||
passCount++;
|
||||
} else {
|
||||
// Check if it's close (within 0.3 of threshold)
|
||||
const minRatio = combo.largeText ? 3.0 : 4.5;
|
||||
if (ratio >= minRatio - 0.3) {
|
||||
warning(`${combo.name.padEnd(45)} ${ratioStr.padStart(8)} (< ${threshold}) ⚠`);
|
||||
warnings++;
|
||||
} else {
|
||||
error(`${combo.name.padEnd(45)} ${ratioStr.padStart(8)} (< ${threshold}) ✗`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Summary', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
log(` Combinations Checked: ${colorCombinations.length}`, 'bright');
|
||||
log(` Passed: ${passCount}`, 'green');
|
||||
if (warnings > 0) log(` Warnings: ${warnings}`, 'yellow');
|
||||
if (failCount > 0) log(` Failed: ${failCount}`, 'red');
|
||||
console.log('');
|
||||
|
||||
if (failCount > 0) {
|
||||
error('Some color combinations fail WCAG AA standards');
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
} else if (warnings > 0) {
|
||||
warning('All combinations pass, but some are borderline');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
} else {
|
||||
success('All color combinations meet WCAG AA standards');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* CSP Violation Scanner
|
||||
* Scans HTML and JS files for Content Security Policy violations
|
||||
*
|
||||
* Violations checked (inst_008):
|
||||
* - Inline event handlers (onclick, onload, etc.)
|
||||
* - Inline styles (style="...")
|
||||
* - Inline scripts (<script>...code...</script>)
|
||||
* - javascript: URLs
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/check-csp-violations.js [pattern]
|
||||
*
|
||||
* Examples:
|
||||
* node scripts/check-csp-violations.js
|
||||
* node scripts/check-csp-violations.js public
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Default patterns to scan
|
||||
const DEFAULT_PATTERNS = [
|
||||
'public/**/*.html',
|
||||
'public/**/*.js'
|
||||
];
|
||||
|
||||
// CSP violation patterns
|
||||
const VIOLATION_PATTERNS = {
|
||||
inline_event_handlers: {
|
||||
regex: /\s(on[a-z]+)=["'][^"']*["']/gi,
|
||||
description: 'Inline event handler',
|
||||
severity: 'HIGH'
|
||||
},
|
||||
inline_styles: {
|
||||
regex: /\sstyle=["'][^"']*["']/gi,
|
||||
description: 'Inline style attribute',
|
||||
severity: 'HIGH'
|
||||
},
|
||||
inline_scripts: {
|
||||
regex: /<script(?![^>]*\ssrc=)[^>]*>[\s\S]*?<\/script>/gi,
|
||||
description: 'Inline script block',
|
||||
severity: 'CRITICAL'
|
||||
},
|
||||
javascript_urls: {
|
||||
regex: /href=["']javascript:/gi,
|
||||
description: 'javascript: URL',
|
||||
severity: 'CRITICAL'
|
||||
}
|
||||
};
|
||||
|
||||
// Files to exclude from scanning
|
||||
const EXCLUDED_PATTERNS = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'dist',
|
||||
'build'
|
||||
];
|
||||
|
||||
/**
|
||||
* Find files matching glob patterns
|
||||
*/
|
||||
function findFiles(patterns) {
|
||||
const allPatterns = Array.isArray(patterns) ? patterns : [patterns];
|
||||
const files = new Set();
|
||||
|
||||
allPatterns.forEach(pattern => {
|
||||
try {
|
||||
const result = execSync(`find public -type f \\( -name "*.html" -o -name "*.js" \\) 2>/dev/null`, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024
|
||||
});
|
||||
|
||||
result.split('\n').filter(f => f.trim()).forEach(file => {
|
||||
// Exclude patterns
|
||||
if (!EXCLUDED_PATTERNS.some(excluded => file.includes(excluded))) {
|
||||
files.add(file);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore errors from find command
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(files).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single file for CSP violations
|
||||
*/
|
||||
function scanFile(filePath) {
|
||||
const violations = [];
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Check each violation pattern
|
||||
Object.entries(VIOLATION_PATTERNS).forEach(([type, pattern]) => {
|
||||
const matches = content.matchAll(pattern.regex);
|
||||
|
||||
for (const match of matches) {
|
||||
// Find line number
|
||||
const beforeMatch = content.substring(0, match.index);
|
||||
const lineNumber = beforeMatch.split('\n').length;
|
||||
|
||||
// Get the matched text (truncate if too long)
|
||||
let matchedText = match[0];
|
||||
if (matchedText.length > 80) {
|
||||
matchedText = matchedText.substring(0, 77) + '...';
|
||||
}
|
||||
|
||||
violations.push({
|
||||
file: filePath,
|
||||
line: lineNumber,
|
||||
type,
|
||||
description: pattern.description,
|
||||
severity: pattern.severity,
|
||||
matched: matchedText
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error scanning ${filePath}:`, error.message);
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main scanner function
|
||||
*/
|
||||
function scanForViolations(patterns = DEFAULT_PATTERNS) {
|
||||
console.log('🔍 Scanning for CSP violations...\n');
|
||||
|
||||
const files = findFiles(patterns);
|
||||
console.log(`Found ${files.length} files to scan\n`);
|
||||
|
||||
const allViolations = [];
|
||||
|
||||
files.forEach(file => {
|
||||
const violations = scanFile(file);
|
||||
if (violations.length > 0) {
|
||||
allViolations.push(...violations);
|
||||
}
|
||||
});
|
||||
|
||||
return allViolations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and display violations
|
||||
*/
|
||||
function displayViolations(violations) {
|
||||
if (violations.length === 0) {
|
||||
console.log('✅ No CSP violations found!\n');
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`❌ Found ${violations.length} CSP violation(s):\n`);
|
||||
|
||||
// Group by file
|
||||
const byFile = {};
|
||||
violations.forEach(v => {
|
||||
if (!byFile[v.file]) byFile[v.file] = [];
|
||||
byFile[v.file].push(v);
|
||||
});
|
||||
|
||||
// Display by file
|
||||
Object.entries(byFile).forEach(([file, fileViolations]) => {
|
||||
console.log(`📄 ${file} (${fileViolations.length} violation(s)):`);
|
||||
fileViolations.forEach(v => {
|
||||
const severity = v.severity === 'CRITICAL' ? '🔴' : '🟡';
|
||||
console.log(` ${severity} Line ${v.line}: ${v.description}`);
|
||||
console.log(` ${v.matched}`);
|
||||
});
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// Summary by type
|
||||
console.log('📊 Violations by type:');
|
||||
const byType = {};
|
||||
violations.forEach(v => {
|
||||
byType[v.description] = (byType[v.description] || 0) + 1;
|
||||
});
|
||||
Object.entries(byType).forEach(([type, count]) => {
|
||||
console.log(` - ${type}: ${count}`);
|
||||
});
|
||||
|
||||
console.log(`\n💡 To fix violations, run: node scripts/fix-csp-violations.js\n`);
|
||||
|
||||
return violations.length;
|
||||
}
|
||||
|
||||
// Run scanner if called directly
|
||||
if (require.main === module) {
|
||||
const pattern = process.argv[2] || DEFAULT_PATTERNS;
|
||||
const violations = scanForViolations(pattern);
|
||||
const exitCode = displayViolations(violations);
|
||||
|
||||
process.exit(exitCode > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
module.exports = {
|
||||
scanForViolations,
|
||||
displayViolations,
|
||||
scanFile
|
||||
};
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Check which of the 34 public documents are missing PDFs
|
||||
*/
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const PUBLIC_CATEGORIES = [
|
||||
'getting-started',
|
||||
'technical-reference',
|
||||
'research-theory',
|
||||
'advanced-topics',
|
||||
'case-studies',
|
||||
'business-leadership',
|
||||
'archives'
|
||||
];
|
||||
|
||||
async function main() {
|
||||
await connect();
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
const mongoose = require('mongoose');
|
||||
const { getCollection } = require('../src/utils/db.util');
|
||||
|
||||
const collection = await getCollection('documents');
|
||||
|
||||
const docs = await collection.find({
|
||||
category: { $in: PUBLIC_CATEGORIES }
|
||||
}).sort({ category: 1, order: 1 }).toArray();
|
||||
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('PDF AVAILABILITY CHECK - 34 Public Documents');
|
||||
console.log('='.repeat(70) + '\n');
|
||||
|
||||
const pdfDir = '/home/theflow/projects/tractatus/public/downloads';
|
||||
|
||||
let missingCount = 0;
|
||||
let presentCount = 0;
|
||||
const missing = [];
|
||||
|
||||
for (const doc of docs) {
|
||||
const slug = doc.slug;
|
||||
const pdfPath = path.join(pdfDir, `${slug}.pdf`);
|
||||
|
||||
try {
|
||||
await fs.access(pdfPath);
|
||||
presentCount++;
|
||||
// console.log(`✅ ${doc.title}`);
|
||||
} catch (err) {
|
||||
missingCount++;
|
||||
missing.push({ title: doc.title, slug, category: doc.category });
|
||||
console.log(`❌ MISSING PDF: ${doc.title}`);
|
||||
console.log(` slug: ${slug}`);
|
||||
console.log(` category: ${doc.category}`);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log(`\n📊 Summary:`);
|
||||
console.log(` ✅ PDFs present: ${presentCount}/${docs.length}`);
|
||||
console.log(` ❌ PDFs missing: ${missingCount}/${docs.length}`);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.log(`\n📋 Missing PDFs by category:`);
|
||||
const byCategory = {};
|
||||
missing.forEach(m => {
|
||||
if (!byCategory[m.category]) byCategory[m.category] = [];
|
||||
byCategory[m.category].push(m);
|
||||
});
|
||||
|
||||
Object.keys(byCategory).forEach(cat => {
|
||||
console.log(`\n ${cat}: ${byCategory[cat].length} missing`);
|
||||
byCategory[cat].forEach(m => {
|
||||
console.log(` - ${m.slug}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(70) + '\n');
|
||||
|
||||
await close();
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
const { MongoClient } = require('mongodb');
|
||||
|
||||
(async () => {
|
||||
const client = new MongoClient('mongodb://localhost:27017/tractatus_dev');
|
||||
await client.connect();
|
||||
const docs = await client.db('tractatus_dev').collection('documents')
|
||||
.find({}, { projection: { slug: 1, title: 1, sections: 1, category: 1 } })
|
||||
.sort({ slug: 1 })
|
||||
.toArray();
|
||||
|
||||
const withSections = docs.filter(d => d.sections && d.sections.length > 0);
|
||||
const without = docs.filter(d => !d.sections || d.sections.length === 0);
|
||||
|
||||
console.log('=== DOCUMENTS WITH SECTIONS (' + withSections.length + ') ===');
|
||||
withSections.forEach(d => console.log('-', d.slug, '(' + d.sections.length + ' sections, cat:' + (d.category || 'none') + ')'));
|
||||
|
||||
console.log('\n=== DOCUMENTS WITHOUT SECTIONS (' + without.length + ') ===');
|
||||
without.forEach(d => console.log('-', d.slug, '(cat:' + (d.category || 'none') + ')'));
|
||||
|
||||
await client.close();
|
||||
})();
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Database Cleanup Script
|
||||
* Removes junk documents that should never have been imported into MongoDB
|
||||
*
|
||||
* Categories:
|
||||
* 1. Internal session notes (10 documents)
|
||||
* 2. Obsolete Phase 2 documents (9 documents)
|
||||
* 3. Confidential document duplicates (5 documents - keep filesystem .md files)
|
||||
*
|
||||
* Total: 24 documents to delete
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { getDb } = require('../src/utils/db.util');
|
||||
|
||||
// Documents to delete organized by category (actual slugs from database)
|
||||
const documentsToDelete = {
|
||||
sessionNotes: [
|
||||
'ai-features-implementation-session-2025-10-07',
|
||||
'session-handoff-2025-10-07',
|
||||
'session-handoff-2025-10-07-part-4-governance-active-progress-review',
|
||||
'session-handoff-crossreferencevalidator-debugging',
|
||||
'session-handoff-tractatus-framework-activation'
|
||||
],
|
||||
|
||||
obsoletePhase2: [
|
||||
'phase-2-cost-estimates-hosting-api-usage',
|
||||
'phase-2-deployment-guide-granular-task-instructions',
|
||||
'phase-2-infrastructure-plan',
|
||||
'phase-2-kickoff-checklist',
|
||||
'phase-2-preparation-advisory',
|
||||
'phase-2-production-deployment-ai-features',
|
||||
'phase-2-progress-report-week-5',
|
||||
'phase-2-roadmap-production-deployment-ai-powered-features',
|
||||
'phase-2-soft-launch-email-templates',
|
||||
'tractatus-blog-post-outlines',
|
||||
'tractatus-governance-framework-test-suite-improvement-session-part-2',
|
||||
'tractatus-production-comprehensive-testing-checklist',
|
||||
'tractatus-production-testing-results',
|
||||
'tractatus-project-implementation-progress-report'
|
||||
],
|
||||
|
||||
confidentialDuplicates: [
|
||||
'appendix-e-contact-information',
|
||||
'cover-letter-for-anthropic-submission',
|
||||
'koha-stripe-payment-setup-guide',
|
||||
'security-audit-report'
|
||||
]
|
||||
};
|
||||
|
||||
async function cleanupDatabase() {
|
||||
console.log('🧹 Starting database cleanup...\n');
|
||||
|
||||
const db = await getDb();
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Track results
|
||||
const results = {
|
||||
sessionNotes: { deleted: 0, notFound: [] },
|
||||
obsoletePhase2: { deleted: 0, notFound: [] },
|
||||
confidentialDuplicates: { deleted: 0, notFound: [] }
|
||||
};
|
||||
|
||||
// Delete session notes
|
||||
console.log('📋 Deleting internal session notes...');
|
||||
for (const slug of documentsToDelete.sessionNotes) {
|
||||
const result = await collection.deleteOne({ slug });
|
||||
if (result.deletedCount > 0) {
|
||||
console.log(` ✅ Deleted: ${slug}`);
|
||||
results.sessionNotes.deleted++;
|
||||
} else {
|
||||
console.log(` ⚠️ Not found: ${slug}`);
|
||||
results.sessionNotes.notFound.push(slug);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete obsolete Phase 2 documents
|
||||
console.log('\n📦 Deleting obsolete Phase 2 documents...');
|
||||
for (const slug of documentsToDelete.obsoletePhase2) {
|
||||
const result = await collection.deleteOne({ slug });
|
||||
if (result.deletedCount > 0) {
|
||||
console.log(` ✅ Deleted: ${slug}`);
|
||||
results.obsoletePhase2.deleted++;
|
||||
} else {
|
||||
console.log(` ⚠️ Not found: ${slug}`);
|
||||
results.obsoletePhase2.notFound.push(slug);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete confidential duplicates
|
||||
console.log('\n🔒 Deleting confidential document duplicates from MongoDB...');
|
||||
console.log(' (Filesystem .md files will be preserved)');
|
||||
for (const slug of documentsToDelete.confidentialDuplicates) {
|
||||
const result = await collection.deleteOne({ slug });
|
||||
if (result.deletedCount > 0) {
|
||||
console.log(` ✅ Deleted: ${slug}`);
|
||||
results.confidentialDuplicates.deleted++;
|
||||
} else {
|
||||
console.log(` ⚠️ Not found: ${slug}`);
|
||||
results.confidentialDuplicates.notFound.push(slug);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('📊 Cleanup Summary');
|
||||
console.log('='.repeat(70));
|
||||
console.log(`Session Notes: ${results.sessionNotes.deleted}/${documentsToDelete.sessionNotes.length} deleted`);
|
||||
console.log(`Obsolete Phase 2: ${results.obsoletePhase2.deleted}/${documentsToDelete.obsoletePhase2.length} deleted`);
|
||||
console.log(`Confidential Duplicates: ${results.confidentialDuplicates.deleted}/${documentsToDelete.confidentialDuplicates.length} deleted`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const totalDeleted = results.sessionNotes.deleted + results.obsoletePhase2.deleted + results.confidentialDuplicates.deleted;
|
||||
const totalExpected = documentsToDelete.sessionNotes.length + documentsToDelete.obsoletePhase2.length + documentsToDelete.confidentialDuplicates.length;
|
||||
|
||||
console.log(`\n✅ Total deleted: ${totalDeleted}/${totalExpected} documents\n`);
|
||||
|
||||
// Report not found documents
|
||||
const allNotFound = [
|
||||
...results.sessionNotes.notFound,
|
||||
...results.obsoletePhase2.notFound,
|
||||
...results.confidentialDuplicates.notFound
|
||||
];
|
||||
|
||||
if (allNotFound.length > 0) {
|
||||
console.log('⚠️ Documents not found (may have been deleted already):');
|
||||
allNotFound.forEach(slug => console.log(` - ${slug}`));
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Get final document count
|
||||
const finalCount = await collection.countDocuments();
|
||||
console.log(`📚 Remaining documents in database: ${finalCount}\n`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run cleanup
|
||||
cleanupDatabase().catch(error => {
|
||||
console.error('❌ Cleanup failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Compare Dev and Prod Databases
|
||||
* Identifies documents that exist in one but not the other
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
async function compareDatabases() {
|
||||
// Connect to local dev
|
||||
console.log('🔌 Connecting to local dev database...');
|
||||
const devClient = new MongoClient('mongodb://localhost:27017/tractatus_dev');
|
||||
await devClient.connect();
|
||||
const devDb = devClient.db('tractatus_dev');
|
||||
const devColl = devDb.collection('documents');
|
||||
|
||||
// Connect to local prod (for comparison)
|
||||
console.log('🔌 Connecting to local prod database...\n');
|
||||
const prodClient = new MongoClient('mongodb://localhost:27017/tractatus_prod');
|
||||
await prodClient.connect();
|
||||
const prodDb = prodClient.db('tractatus_prod');
|
||||
const prodColl = prodDb.collection('documents');
|
||||
|
||||
// Get all documents
|
||||
const devDocs = await devColl.find({}).project({ slug: 1, title: 1, visibility: 1 }).sort({ slug: 1 }).toArray();
|
||||
const prodDocs = await prodColl.find({}).project({ slug: 1, title: 1, visibility: 1 }).sort({ slug: 1 }).toArray();
|
||||
|
||||
const devSlugs = new Set(devDocs.map(d => d.slug));
|
||||
const prodSlugs = new Set(prodDocs.map(d => d.slug));
|
||||
|
||||
const inDevNotProd = devDocs.filter(d => !prodSlugs.has(d.slug));
|
||||
const inProdNotDev = prodDocs.filter(d => !devSlugs.has(d.slug));
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('DATABASE COMPARISON');
|
||||
console.log('='.repeat(70));
|
||||
console.log(`Dev total: ${devDocs.length}`);
|
||||
console.log(`Prod total: ${prodDocs.length}`);
|
||||
console.log(`Difference: ${Math.abs(devDocs.length - prodDocs.length)}`);
|
||||
console.log('='.repeat(70));
|
||||
|
||||
if (inDevNotProd.length > 0) {
|
||||
console.log(`\n📋 Documents in DEV but NOT in PROD (${inDevNotProd.length}):\n`);
|
||||
inDevNotProd.forEach(d => {
|
||||
console.log(` - ${d.slug} [${d.visibility || 'public'}]`);
|
||||
});
|
||||
}
|
||||
|
||||
if (inProdNotDev.length > 0) {
|
||||
console.log(`\n📋 Documents in PROD but NOT in DEV (${inProdNotDev.length}):\n`);
|
||||
inProdNotDev.forEach(d => {
|
||||
console.log(` - ${d.slug} [${d.visibility || 'public'}]`);
|
||||
});
|
||||
}
|
||||
|
||||
if (inDevNotProd.length === 0 && inProdNotDev.length === 0) {
|
||||
console.log('\n✅ Dev and Prod have identical document sets!\n');
|
||||
}
|
||||
|
||||
await devClient.close();
|
||||
await prodClient.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
compareDatabases().catch(error => {
|
||||
console.error('❌ Comparison failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
const bcrypt = require('bcrypt');
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
async function createAdmin() {
|
||||
const client = new MongoClient('mongodb://localhost:27017');
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db('tractatus_dev');
|
||||
|
||||
// Delete existing admin
|
||||
await db.collection('users').deleteOne({ email: 'admin@tractatus.local' });
|
||||
|
||||
// Create new admin with hashed password
|
||||
const password = bcrypt.hashSync('tractatus2025', 12);
|
||||
await db.collection('users').insertOne({
|
||||
name: 'Admin User',
|
||||
email: 'admin@tractatus.local',
|
||||
password: password,
|
||||
role: 'admin',
|
||||
active: true,
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
console.log('✅ Admin user created: admin@tractatus.local / tractatus2025');
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
createAdmin().catch(console.error);
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fix CSP violations in admin JS files
|
||||
* - Replace inline styles with classes and data attributes
|
||||
* - Replace inline event handlers with event delegation
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Fix auth-check.js inline styles
|
||||
function fixAuthCheck() {
|
||||
const filePath = path.join(__dirname, '../public/js/admin/auth-check.js');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const oldHTML = ` <div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, -apple-system, sans-serif;">
|
||||
<div style="text-align: center;">
|
||||
<svg style="width: 64px; height: 64px; margin: 0 auto 16px; color: #3B82F6;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
<h2 style="font-size: 20px; font-weight: 600; color: #111827; margin-bottom: 8px;">Authentication Required</h2>
|
||||
<p style="color: #6B7280; margin-bottom: 16px;">\${reason}</p>
|
||||
<p style="color: #9CA3AF; font-size: 14px;">Redirecting to login...</p>`;
|
||||
|
||||
const newHTML = ` <div class="auth-error-container">
|
||||
<div class="auth-error-content">
|
||||
<svg class="auth-error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
<h2 class="auth-error-title">Authentication Required</h2>
|
||||
<p class="auth-error-message">\${reason}</p>
|
||||
<p class="auth-error-redirect">Redirecting to login...</p>`;
|
||||
|
||||
if (content.includes(oldHTML)) {
|
||||
content = content.replace(oldHTML, newHTML);
|
||||
fs.writeFileSync(filePath, content);
|
||||
return 6; // 6 inline styles fixed
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fix progress bar widths by using data attributes
|
||||
function fixProgressBars() {
|
||||
const files = [
|
||||
'public/js/admin/audit-analytics.js',
|
||||
'public/js/admin/rule-editor.js',
|
||||
'public/js/admin/rule-manager.js'
|
||||
];
|
||||
|
||||
let totalFixed = 0;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(__dirname, '..', file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let fileFixed = 0;
|
||||
|
||||
// Pattern 1: <div ... style="width: ${var}%"></div>
|
||||
const pattern1 = /(<div[^>]*class="[^"]*(?:bg-blue-600|bg-green-500|bg-blue-500|bg-yellow-500)[^"]*"[^>]*)\s+style="width:\s*\$\{([^}]+)\}%"/g;
|
||||
content = content.replace(pattern1, (match, before, variable) => {
|
||||
fileFixed++;
|
||||
return `${before} data-width="\${${variable}}"`;
|
||||
});
|
||||
|
||||
// Pattern 2: <div ... style="width: 0%"> or style="width: 100%">
|
||||
const pattern2 = /(<div[^>]*(?:id="[^"]*-bar")[^>]*)\s+style="width:\s*(0|100)%"/g;
|
||||
content = content.replace(pattern2, (match, before, value) => {
|
||||
fileFixed++;
|
||||
return `${before} data-width="${value}"`;
|
||||
});
|
||||
|
||||
// Pattern 3: style="height: ${barHeight}%"
|
||||
const pattern3 = /style="height:\s*\$\{([^}]+)\}%"/g;
|
||||
content = content.replace(pattern3, (match, variable) => {
|
||||
fileFixed++;
|
||||
return `data-height="\${${variable}}"`;
|
||||
});
|
||||
|
||||
if (fileFixed > 0) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`✓ ${file}: Fixed ${fileFixed} inline style(s)`);
|
||||
totalFixed += fileFixed;
|
||||
}
|
||||
});
|
||||
|
||||
return totalFixed;
|
||||
}
|
||||
|
||||
// Add width-setting helper after DOM insertion
|
||||
function addProgressBarHelper() {
|
||||
const files = [
|
||||
{ file: 'public/js/admin/audit-analytics.js', hasProgressBars: true },
|
||||
{ file: 'public/js/admin/rule-editor.js', hasProgressBars: true },
|
||||
{ file: 'public/js/admin/rule-manager.js', hasProgressBars: true }
|
||||
];
|
||||
|
||||
const helper = `
|
||||
// Set widths from data attributes (CSP compliance)
|
||||
function setProgressBarWidths(container) {
|
||||
const elements = container.querySelectorAll('[data-width], [data-height]');
|
||||
elements.forEach(el => {
|
||||
if (el.dataset.width) {
|
||||
el.style.width = el.dataset.width + '%';
|
||||
}
|
||||
if (el.dataset.height) {
|
||||
el.style.height = el.dataset.height + '%';
|
||||
}
|
||||
});
|
||||
}`;
|
||||
|
||||
files.forEach(({ file, hasProgressBars }) => {
|
||||
if (!hasProgressBars) return;
|
||||
|
||||
const filePath = path.join(__dirname, '..', file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Check if helper already exists
|
||||
if (content.includes('setProgressBarWidths')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add helper function before the last closing brace/parenthesis of the file
|
||||
// Find a good insertion point - typically after other helper functions
|
||||
const insertionPoint = content.lastIndexOf('})()');
|
||||
if (insertionPoint > 0) {
|
||||
content = content.slice(0, insertionPoint) + helper + '\n\n' + content.slice(insertionPoint);
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`✓ ${file}: Added setProgressBarWidths helper`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Main execution
|
||||
console.log('\n🔧 Fixing admin CSP violations...\n');
|
||||
|
||||
let totalFixed = 0;
|
||||
|
||||
console.log('1. Fixing auth-check.js inline styles...');
|
||||
totalFixed += fixAuthCheck();
|
||||
|
||||
console.log('\n2. Converting progress bar widths to data attributes...');
|
||||
totalFixed += fixProgressBars();
|
||||
|
||||
console.log('\n3. Adding progress bar width helpers...');
|
||||
addProgressBarHelper();
|
||||
|
||||
console.log(`\n✅ Total inline styles fixed: ${totalFixed}`);
|
||||
console.log('\n⚠️ Note: Inline event handlers require manual refactoring');
|
||||
console.log(' Run scripts/fix-admin-event-handlers.js for those.\n');
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fix inline event handlers in admin JS files
|
||||
* Replace with data attributes and event delegation
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const files = [
|
||||
'public/js/admin/audit-analytics.js',
|
||||
'public/js/admin/claude-md-migrator.js',
|
||||
'public/js/admin/dashboard.js',
|
||||
'public/js/admin/project-editor.js',
|
||||
'public/js/admin/project-manager.js',
|
||||
'public/js/admin/rule-editor.js',
|
||||
'public/js/admin/rule-manager.js'
|
||||
];
|
||||
|
||||
let totalFixed = 0;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(__dirname, '..', file);
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let fileFixed = 0;
|
||||
|
||||
// Pattern 1: onclick="this.parentElement.remove()"
|
||||
const pattern1 = /\s*onclick="this\.parentElement\.remove\(\)"/g;
|
||||
const matches1 = (content.match(pattern1) || []).length;
|
||||
content = content.replace(pattern1, ' data-action="remove-parent"');
|
||||
fileFixed += matches1;
|
||||
|
||||
// Pattern 2: onclick="functionName('arg')" or onclick="functionName('arg', 'arg2')"
|
||||
const pattern2 = /onclick="([a-zA-Z.]+)\(([^)]+)\)"/g;
|
||||
content = content.replace(pattern2, (match, funcName, args) => {
|
||||
fileFixed++;
|
||||
// Extract arguments
|
||||
const argList = args.split(',').map(a => a.trim().replace(/['"]/g, ''));
|
||||
|
||||
// Create data attributes
|
||||
let dataAttrs = `data-action="${funcName.replace('window.', '').replace('projectEditor.', '')}"`;
|
||||
argList.forEach((arg, i) => {
|
||||
if (arg.startsWith('${')) {
|
||||
// Template literal - keep as is
|
||||
dataAttrs += ` data-arg${i}="${arg}"`;
|
||||
} else {
|
||||
// Plain value
|
||||
dataAttrs += ` data-arg${i}="${arg}"`;
|
||||
}
|
||||
});
|
||||
|
||||
return dataAttrs;
|
||||
});
|
||||
|
||||
// Pattern 3: onchange="functionName(...)"
|
||||
const pattern3 = /onchange="([a-zA-Z.]+)\(([^)]+)\)"/g;
|
||||
content = content.replace(pattern3, (match, funcName, args) => {
|
||||
fileFixed++;
|
||||
return `data-change-action="${funcName}" data-change-args="${args}"`;
|
||||
});
|
||||
|
||||
if (fileFixed > 0) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`✓ ${file}: Fixed ${fileFixed} event handler(s)`);
|
||||
totalFixed += fileFixed;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n✅ Total event handlers fixed: ${totalFixed}`);
|
||||
console.log('\n⚠️ Next: Add event delegation listeners to each file\n');
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close, getCollection } = require('../src/utils/db.util');
|
||||
const User = require('../src/models/User.model');
|
||||
|
||||
const EMAIL = process.argv[2] || 'admin@agenticgovernance.digital';
|
||||
const PASSWORD = process.argv[3] || 'TractatusDev2025';
|
||||
const NAME = process.argv[4] || 'Admin User';
|
||||
|
||||
async function fixAdminUser() {
|
||||
try {
|
||||
await connect();
|
||||
|
||||
// Find existing admin user
|
||||
const existing = await User.findByEmail(EMAIL);
|
||||
|
||||
if (existing) {
|
||||
console.log(`✅ Found existing admin: ${existing.email} (ID: ${existing._id})`);
|
||||
console.log(` Deleting...`);
|
||||
await User.delete(existing._id);
|
||||
console.log(`✅ Deleted old admin user`);
|
||||
}
|
||||
|
||||
// Create new admin with proper password field
|
||||
console.log(`\n📝 Creating new admin user...`);
|
||||
const admin = await User.create({
|
||||
name: NAME,
|
||||
email: EMAIL,
|
||||
password: PASSWORD,
|
||||
role: 'admin',
|
||||
active: true
|
||||
});
|
||||
|
||||
console.log(`\n✅ Admin user created successfully!`);
|
||||
console.log(` Email: ${admin.email}`);
|
||||
console.log(` Password: ${PASSWORD}`);
|
||||
console.log(` Role: ${admin.role}`);
|
||||
console.log(` ID: ${admin._id}`);
|
||||
console.log(`\n🔐 Test login at: POST /api/auth/login`);
|
||||
|
||||
await close();
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error:', error.message);
|
||||
await close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fixAdminUser();
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fix Category Mismatches Between Dev and Prod
|
||||
*
|
||||
* Harmonizes document categories to canonical values based on document content and purpose
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { getDb } = require('../src/utils/db.util');
|
||||
|
||||
// Canonical category assignments (based on content analysis)
|
||||
const categoryFixes = {
|
||||
'architectural-overview-and-research-status': 'research-theory',
|
||||
'comparison-matrix-claude-code-claudemd-and-tractatus-framework': 'technical-reference',
|
||||
'executive-brief-tractatus-based-llm-architecture-for-ai-safety': 'research-theory',
|
||||
'pluralistic-values-research-foundations': 'advanced-topics'
|
||||
};
|
||||
|
||||
async function fixCategoryMismatches() {
|
||||
console.log('🔧 Fixing category mismatches...\n');
|
||||
|
||||
const db = await getDb();
|
||||
const collection = db.collection('documents');
|
||||
|
||||
let fixedCount = 0;
|
||||
let notFoundCount = 0;
|
||||
|
||||
for (const [slug, correctCategory] of Object.entries(categoryFixes)) {
|
||||
try {
|
||||
const doc = await collection.findOne({ slug });
|
||||
|
||||
if (!doc) {
|
||||
console.log(`⚠️ Not found: ${slug}`);
|
||||
notFoundCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (doc.category === correctCategory) {
|
||||
console.log(`✓ Already correct: ${slug} (${correctCategory})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update category
|
||||
await collection.updateOne(
|
||||
{ slug },
|
||||
{
|
||||
$set: {
|
||||
category: correctCategory,
|
||||
'metadata.date_updated': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`✅ Fixed: ${slug}`);
|
||||
console.log(` ${doc.category || 'none'} → ${correctCategory}\n`);
|
||||
fixedCount++;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error fixing ${slug}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('📊 Summary:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Total documents to fix: ${Object.keys(categoryFixes).length}`);
|
||||
console.log(`✅ Fixed: ${fixedCount}`);
|
||||
console.log(`⚠️ Not found: ${notFoundCount}`);
|
||||
console.log(`✓ Already correct: ${Object.keys(categoryFixes).length - fixedCount - notFoundCount}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run fix
|
||||
fixCategoryMismatches().catch(error => {
|
||||
console.error('❌ Fix failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fix CSP violations in HTML files
|
||||
* Replaces inline styles with CSS utility classes
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const fixes = [
|
||||
// researcher.html
|
||||
{
|
||||
file: 'public/researcher.html',
|
||||
replacements: [
|
||||
{
|
||||
from: 'class="hover:underline transition-colors" style="color: var(--tractatus-core-end);"',
|
||||
to: 'class="hover:underline transition-colors text-tractatus-link"'
|
||||
},
|
||||
{
|
||||
from: 'class="border-l-4 border border-gray-200 rounded-lg mb-4" style="border-left-color: #8b5cf6;"',
|
||||
to: 'class="border-l-4 border border-gray-200 rounded-lg mb-4 border-l-service-validator"'
|
||||
},
|
||||
{
|
||||
from: 'class="border-l-4 border border-gray-200 rounded-lg" style="border-left-color: #8b5cf6;"',
|
||||
to: 'class="border-l-4 border border-gray-200 rounded-lg border-l-service-validator"'
|
||||
}
|
||||
]
|
||||
},
|
||||
// case-submission.html
|
||||
{
|
||||
file: 'public/case-submission.html',
|
||||
replacements: [
|
||||
{
|
||||
from: 'style="border-left-color: var(--tractatus-core-end);"',
|
||||
to: 'class="border-l-tractatus"'
|
||||
},
|
||||
{
|
||||
from: 'style="color: var(--tractatus-core-end);"',
|
||||
to: 'class="text-tractatus-link"'
|
||||
},
|
||||
{
|
||||
from: 'style="background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);"',
|
||||
to: 'class="bg-gradient-cyan-blue"'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let totalFixed = 0;
|
||||
|
||||
fixes.forEach(({ file, replacements }) => {
|
||||
const filePath = path.join(__dirname, '..', file);
|
||||
|
||||
console.log(`\nProcessing ${file}...`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(` ✗ File not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let fileFixed = 0;
|
||||
|
||||
replacements.forEach(({ from, to }) => {
|
||||
const occurrences = (content.match(new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
||||
|
||||
if (occurrences > 0) {
|
||||
content = content.split(from).join(to);
|
||||
fileFixed += occurrences;
|
||||
console.log(` ✓ Fixed ${occurrences} occurrence(s)`);
|
||||
}
|
||||
});
|
||||
|
||||
if (fileFixed > 0) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
totalFixed += fileFixed;
|
||||
console.log(` ✓ Saved ${file} (${fileFixed} fixes)`);
|
||||
} else {
|
||||
console.log(` • No violations found`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n✓ Total fixes applied: ${totalFixed}\n`);
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fix CSP violations in major HTML files (index.html, architecture.html)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const replacements = [
|
||||
// Layout
|
||||
{ from: ' style="min-height: 64px;"', to: ' class="min-h-16"' },
|
||||
|
||||
// Text shadows
|
||||
{ from: ' style="text-shadow: 0 2px 4px rgba(0,0,0,0.1);"', to: ' class="text-shadow-md"' },
|
||||
{ from: ' style="text-shadow: 0 1px 2px rgba(0,0,0,0.1);"', to: ' class="text-shadow-sm"' },
|
||||
|
||||
// Gradients
|
||||
{ from: ' style="background: linear-gradient(135deg, #64ffda 0%, #448aff 50%, #0ea5e9 100%);"', to: ' class="bg-gradient-tractatus"' },
|
||||
{ from: ' style="background: var(--gradient-btn-validator);"', to: ' class="bg-gradient-service-validator"' },
|
||||
{ from: ' style="background: var(--gradient-btn-instruction);"', to: ' class="bg-gradient-service-instruction"' },
|
||||
{ from: ' style="background: var(--gradient-btn-deliberation);"', to: ' class="bg-gradient-service-deliberation"' },
|
||||
{ from: ' style="background: linear-gradient(135deg, #10b981 0%, #059669 100%);"', to: ' class="bg-gradient-service-boundary"' },
|
||||
{ from: ' style="background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);"', to: ' class="bg-gradient-service-instruction"' },
|
||||
{ from: ' style="background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);"', to: ' class="bg-gradient-service-validator"' },
|
||||
{ from: ' style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);"', to: ' class="bg-gradient-service-pressure"' },
|
||||
{ from: ' style="background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);"', to: ' class="bg-gradient-service-metacognitive"' },
|
||||
{ from: ' style="background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);"', to: ' class="bg-gradient-service-deliberation"' },
|
||||
|
||||
// Border colors
|
||||
{ from: ' style="border-left-color: var(--service-validator-light);"', to: ' class="border-l-service-validator"' },
|
||||
{ from: ' style="border-left-color: var(--service-instruction-light);"', to: ' class="border-l-service-instruction"' },
|
||||
{ from: ' style="border-left-color: var(--service-deliberation-light);"', to: ' class="border-l-service-deliberation"' },
|
||||
{ from: ' style="border-left-color: #10b981;"', to: ' class="border-l-service-boundary"' },
|
||||
{ from: ' style="border-left-color: #6366f1;"', to: ' class="border-l-service-instruction"' },
|
||||
{ from: ' style="border-left-color: #8b5cf6;"', to: ' class="border-l-service-validator"' },
|
||||
{ from: ' style="border-left-color: #f59e0b;"', to: ' class="border-l-service-pressure"' },
|
||||
{ from: ' style="border-left-color: #ec4899;"', to: ' class="border-l-service-metacognitive"' },
|
||||
{ from: ' style="border-left-color: #14b8a6;"', to: ' class="border-l-service-deliberation"' },
|
||||
|
||||
// Text colors
|
||||
{ from: ' style="color: var(--tractatus-core-end);"', to: ' class="text-tractatus-link"' },
|
||||
{ from: ' style="color: var(--service-validator-light);"', to: ' class="text-service-validator"' },
|
||||
{ from: ' style="color: var(--service-instruction-light);"', to: ' class="text-service-instruction"' },
|
||||
{ from: ' style="color: var(--service-deliberation-light);"', to: ' class="text-service-deliberation"' },
|
||||
{ from: ' style="color: var(--service-validator-dark);"', to: ' class="text-service-validator"' },
|
||||
{ from: ' style="color: var(--service-instruction-dark);"', to: ' class="text-service-instruction"' },
|
||||
{ from: ' style="color: var(--service-deliberation-dark);"', to: ' class="text-service-deliberation"' },
|
||||
|
||||
// Badges
|
||||
{ from: ' style="color: #065f46; background-color: #d1fae5;"', to: ' class="badge-boundary"' },
|
||||
{ from: ' style="color: #3730a3; background-color: #e0e7ff;"', to: ' class="badge-instruction"' },
|
||||
{ from: ' style="color: #581c87; background-color: #f3e8ff;"', to: ' class="badge-validator"' },
|
||||
{ from: ' style="color: #92400e; background-color: #fef3c7;"', to: ' class="badge-pressure"' },
|
||||
{ from: ' style="color: #831843; background-color: #fce7f3;"', to: ' class="badge-metacognitive"' },
|
||||
{ from: ' style="color: #134e4a; background-color: #ccfbf1;"', to: ' class="badge-deliberation"' }
|
||||
];
|
||||
|
||||
const files = [
|
||||
'public/index.html',
|
||||
'public/architecture.html'
|
||||
];
|
||||
|
||||
let totalFixed = 0;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(__dirname, '..', file);
|
||||
|
||||
console.log(`\nProcessing ${file}...`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(` ✗ File not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
let fileFixed = 0;
|
||||
|
||||
replacements.forEach(({ from, to }) => {
|
||||
const occurrences = (content.match(new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
||||
|
||||
if (occurrences > 0) {
|
||||
content = content.split(from).join(to);
|
||||
fileFixed += occurrences;
|
||||
}
|
||||
});
|
||||
|
||||
if (fileFixed > 0) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
totalFixed += fileFixed;
|
||||
console.log(` ✓ Fixed ${fileFixed} violation(s)`);
|
||||
} else {
|
||||
console.log(` • No violations found`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n✓ Total fixes applied: ${totalFixed}\n`);
|
||||
|
|
@ -1,290 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* CSP Violation Auto-Remediation Script
|
||||
*
|
||||
* Analyzes CSP violations and provides fix recommendations.
|
||||
* Can optionally attempt automatic fixes for simple cases.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/fix-csp-violations.js [--auto] [file]
|
||||
*
|
||||
* Options:
|
||||
* --auto Attempt automatic fixes (USE WITH CAUTION)
|
||||
* --dry-run Show what would be fixed without making changes
|
||||
* [file] Specific file to fix (default: scan all)
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { scanForViolations, scanFile } = require('./check-csp-violations');
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
bold: '\x1b[1m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command-line arguments
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
return {
|
||||
auto: args.includes('--auto'),
|
||||
dryRun: args.includes('--dry-run'),
|
||||
file: args.find(arg => !arg.startsWith('--'))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fix recommendations for a violation
|
||||
*/
|
||||
function generateFixRecommendation(violation) {
|
||||
const recommendations = {
|
||||
inline_event_handlers: {
|
||||
priority: 'HIGH',
|
||||
approach: 'Move to external JavaScript',
|
||||
steps: [
|
||||
`1. Create event listener in external JS file:`,
|
||||
` document.getElementById('element-id').addEventListener('click', function() {`,
|
||||
` // Handler code here`,
|
||||
` });`,
|
||||
``,
|
||||
`2. Remove ${violation.matched.split('=')[0]}= attribute from HTML`,
|
||||
``,
|
||||
`3. Add unique ID to element if needed for selection`
|
||||
],
|
||||
example: 'See public/js/components/*.js for examples'
|
||||
},
|
||||
inline_styles: {
|
||||
priority: 'HIGH',
|
||||
approach: 'Move to Tailwind CSS classes or external CSS',
|
||||
steps: [
|
||||
`1. For dynamic styles: Use CSS classes with JavaScript`,
|
||||
` element.classList.add('custom-style');`,
|
||||
``,
|
||||
`2. For static styles: Add Tailwind classes to HTML`,
|
||||
` Replace style="${violation.matched}" with Tailwind utilities`,
|
||||
``,
|
||||
`3. For complex styles: Add to public/css/custom.css`
|
||||
],
|
||||
example: 'Project uses Tailwind CSS - prefer utility classes'
|
||||
},
|
||||
inline_scripts: {
|
||||
priority: 'CRITICAL',
|
||||
approach: 'Extract to external JavaScript file',
|
||||
steps: [
|
||||
`1. Create or identify appropriate JS file in public/js/`,
|
||||
``,
|
||||
`2. Move script content to external file`,
|
||||
``,
|
||||
`3. Replace inline script with:`,
|
||||
` <script src="/js/your-file.js"></script>`,
|
||||
``,
|
||||
`4. Ensure script loads at appropriate time (defer/async if needed)`
|
||||
],
|
||||
example: 'See public/js/*.js for existing patterns'
|
||||
},
|
||||
javascript_urls: {
|
||||
priority: 'CRITICAL',
|
||||
approach: 'Replace with proper event handlers',
|
||||
steps: [
|
||||
`1. Remove href="javascript:..." attribute`,
|
||||
``,
|
||||
`2. Add event listener in external JS:`,
|
||||
` document.getElementById('link-id').addEventListener('click', function(e) {`,
|
||||
` e.preventDefault();`,
|
||||
` // Action code here`,
|
||||
` });`,
|
||||
``,
|
||||
`3. For links that don't navigate, consider using <button> instead of <a>`
|
||||
],
|
||||
example: 'Use semantic HTML and event listeners'
|
||||
}
|
||||
};
|
||||
|
||||
return recommendations[violation.type] || {
|
||||
priority: 'MEDIUM',
|
||||
approach: 'Manual review required',
|
||||
steps: ['Review violation and apply appropriate CSP-compliant fix'],
|
||||
example: ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display fix recommendations for violations
|
||||
*/
|
||||
function displayFixRecommendations(violations) {
|
||||
if (violations.length === 0) {
|
||||
log('\n✅ No violations to fix!\n', 'green');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`\n${'='.repeat(70)}`, 'cyan');
|
||||
log(`CSP VIOLATION FIX RECOMMENDATIONS`, 'bold');
|
||||
log(`${'='.repeat(70)}\n`, 'cyan');
|
||||
|
||||
// Group by file
|
||||
const byFile = {};
|
||||
violations.forEach(v => {
|
||||
if (!byFile[v.file]) byFile[v.file] = [];
|
||||
byFile[v.file].push(v);
|
||||
});
|
||||
|
||||
Object.entries(byFile).forEach(([file, fileViolations]) => {
|
||||
log(`\n📄 ${file}`, 'cyan');
|
||||
log(` ${fileViolations.length} violation(s)\n`, 'yellow');
|
||||
|
||||
fileViolations.forEach((violation, idx) => {
|
||||
const rec = generateFixRecommendation(violation);
|
||||
|
||||
log(` ${idx + 1}. Line ${violation.line}: ${violation.description}`, 'bold');
|
||||
log(` ${violation.matched}`, 'yellow');
|
||||
log(``, 'reset');
|
||||
log(` Priority: ${rec.priority}`, rec.priority === 'CRITICAL' ? 'red' : 'yellow');
|
||||
log(` Approach: ${rec.approach}`, 'cyan');
|
||||
log(``, 'reset');
|
||||
|
||||
rec.steps.forEach(step => {
|
||||
log(` ${step}`, 'reset');
|
||||
});
|
||||
|
||||
if (rec.example) {
|
||||
log(``, 'reset');
|
||||
log(` 💡 ${rec.example}`, 'green');
|
||||
}
|
||||
|
||||
log(``, 'reset');
|
||||
});
|
||||
});
|
||||
|
||||
log(`${'='.repeat(70)}`, 'cyan');
|
||||
log(`\n📊 Summary:`, 'bold');
|
||||
log(` Total violations: ${violations.length}`, 'yellow');
|
||||
|
||||
const byType = {};
|
||||
violations.forEach(v => {
|
||||
byType[v.description] = (byType[v.description] || 0) + 1;
|
||||
});
|
||||
|
||||
Object.entries(byType).forEach(([type, count]) => {
|
||||
log(` - ${type}: ${count}`, 'reset');
|
||||
});
|
||||
|
||||
log(`\n⚠️ IMPORTANT:`, 'yellow');
|
||||
log(` - Test all changes locally before deployment`, 'reset');
|
||||
log(` - Verify functionality after removing inline code`, 'reset');
|
||||
log(` - Use browser DevTools to debug event handlers`, 'reset');
|
||||
log(` - Run npm run check:csp after fixes to verify\n`, 'reset');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt automatic fix for simple violations (EXPERIMENTAL)
|
||||
*/
|
||||
function attemptAutoFix(violations, dryRun = false) {
|
||||
log(`\n⚠️ AUTO-FIX MODE (EXPERIMENTAL)\n`, 'yellow');
|
||||
|
||||
if (dryRun) {
|
||||
log(` DRY RUN - No files will be modified\n`, 'cyan');
|
||||
} else {
|
||||
log(` ⚠️ FILES WILL BE MODIFIED - Ensure you have backups!\n`, 'red');
|
||||
}
|
||||
|
||||
let fixCount = 0;
|
||||
const fixableTypes = ['inline_styles']; // Only auto-fix inline styles for now
|
||||
|
||||
violations.forEach(violation => {
|
||||
if (fixableTypes.includes(violation.type)) {
|
||||
log(` ${dryRun ? '[DRY RUN]' : '[FIXING]'} ${violation.file}:${violation.line}`, 'cyan');
|
||||
log(` ${violation.matched}`, 'yellow');
|
||||
|
||||
// For now, just count fixable violations
|
||||
// Actual fixing would require careful parsing and replacement
|
||||
fixCount++;
|
||||
|
||||
log(` → Would convert to Tailwind classes or external CSS`, 'green');
|
||||
log(``, 'reset');
|
||||
} else {
|
||||
log(` [SKIP] ${violation.file}:${violation.line} - ${violation.type}`, 'yellow');
|
||||
log(` Requires manual fix`, 'reset');
|
||||
log(``, 'reset');
|
||||
}
|
||||
});
|
||||
|
||||
log(`\n📊 Auto-fix summary:`, 'bold');
|
||||
log(` Fixable: ${fixCount}`, 'green');
|
||||
log(` Manual: ${violations.length - fixCount}\n`, 'yellow');
|
||||
|
||||
if (!dryRun && fixCount > 0) {
|
||||
log(` ⚠️ NOTE: Auto-fix is experimental and disabled by default`, 'red');
|
||||
log(` Use --dry-run to preview changes, then apply manually\n`, 'yellow');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
log(`\n${'='.repeat(70)}`, 'cyan');
|
||||
log(`CSP Violation Remediation Tool`, 'bold');
|
||||
log(`${'='.repeat(70)}\n`, 'cyan');
|
||||
|
||||
// Scan for violations
|
||||
log(`🔍 Scanning for violations...\n`, 'cyan');
|
||||
|
||||
let violations;
|
||||
if (args.file) {
|
||||
log(` Target: ${args.file}\n`, 'yellow');
|
||||
violations = scanFile(args.file);
|
||||
} else {
|
||||
log(` Target: All public files\n`, 'yellow');
|
||||
violations = scanForViolations();
|
||||
}
|
||||
|
||||
log(` Found: ${violations.length} violation(s)\n`, violations.length > 0 ? 'yellow' : 'green');
|
||||
|
||||
if (violations.length === 0) {
|
||||
log(`✅ No violations found!\n`, 'green');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Auto-fix mode
|
||||
if (args.auto || args.dryRun) {
|
||||
attemptAutoFix(violations, args.dryRun);
|
||||
} else {
|
||||
// Display recommendations
|
||||
displayFixRecommendations(violations);
|
||||
}
|
||||
|
||||
log(`\n💡 Next steps:`, 'cyan');
|
||||
log(` 1. Review recommendations above`, 'reset');
|
||||
log(` 2. Apply fixes manually or use --dry-run to preview`, 'reset');
|
||||
log(` 3. Test changes locally (npm start on port 9000)`, 'reset');
|
||||
log(` 4. Verify fixes: npm run check:csp`, 'reset');
|
||||
log(` 5. Deploy once all violations are resolved\n`, 'reset');
|
||||
|
||||
process.exit(violations.length > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateFixRecommendation,
|
||||
displayFixRecommendations
|
||||
};
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Fix remaining gradient violations in index.html
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, '../public/index.html');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const replacements = [
|
||||
{ from: ' style="background: var(--gradient-btn-boundary);"', to: ' class="bg-gradient-service-boundary"' },
|
||||
{ from: ' style="background: var(--gradient-btn-pressure);"', to: ' class="bg-gradient-service-pressure"' },
|
||||
{ from: ' style="background: var(--gradient-btn-metacognitive);"', to: ' class="bg-gradient-service-metacognitive"' }
|
||||
];
|
||||
|
||||
let fixed = 0;
|
||||
replacements.forEach(({ from, to }) => {
|
||||
const count = (content.match(new RegExp(from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
|
||||
if (count > 0) {
|
||||
content = content.split(from).join(to);
|
||||
fixed += count;
|
||||
console.log(`✓ Fixed ${count} occurrence(s) of ${from.substring(0, 40)}...`);
|
||||
}
|
||||
});
|
||||
|
||||
if (fixed > 0) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`\n✓ Total fixes applied to index.html: ${fixed}\n`);
|
||||
} else {
|
||||
console.log('\n• No violations found\n');
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate PDF from Architectural Safeguards Against LLM Hierarchical Dominance (Prose version)
|
||||
"""
|
||||
|
||||
import markdown
|
||||
from weasyprint import HTML, CSS
|
||||
from pathlib import Path
|
||||
|
||||
def generate_pdf():
|
||||
"""Convert the prose markdown document to PDF with professional styling"""
|
||||
|
||||
# Paths
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir.parent
|
||||
input_file = project_root / 'docs' / 'research' / 'ARCHITECTURAL-SAFEGUARDS-Against-LLM-Hierarchical-Dominance-Prose.md'
|
||||
output_file = project_root / 'docs' / 'research' / 'ARCHITECTURAL-SAFEGUARDS-Against-LLM-Hierarchical-Dominance-Prose.pdf'
|
||||
|
||||
print(f"Reading markdown from: {input_file}")
|
||||
|
||||
# Read markdown content
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
md_content = f.read()
|
||||
|
||||
print("Converting markdown to HTML...")
|
||||
|
||||
# Convert markdown to HTML with extensions
|
||||
html_content = markdown.markdown(
|
||||
md_content,
|
||||
extensions=[
|
||||
'markdown.extensions.tables',
|
||||
'markdown.extensions.fenced_code',
|
||||
'markdown.extensions.toc',
|
||||
'markdown.extensions.sane_lists'
|
||||
]
|
||||
)
|
||||
|
||||
# Wrap in full HTML document with professional styling
|
||||
full_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Architectural Safeguards Against LLM Hierarchical Dominance</title>
|
||||
</head>
|
||||
<body>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Professional CSS styling
|
||||
css = CSS(string="""
|
||||
@page {
|
||||
size: Letter;
|
||||
margin: 1in;
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Georgia", "Times New Roman", serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24pt;
|
||||
font-weight: bold;
|
||||
color: #1976d2;
|
||||
margin-top: 24pt;
|
||||
margin-bottom: 12pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
color: #1976d2;
|
||||
margin-top: 20pt;
|
||||
margin-bottom: 10pt;
|
||||
page-break-after: avoid;
|
||||
border-bottom: 2px solid #1976d2;
|
||||
padding-bottom: 4pt;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
color: #424242;
|
||||
margin-top: 16pt;
|
||||
margin-bottom: 8pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 12pt;
|
||||
font-weight: bold;
|
||||
color: #424242;
|
||||
margin-top: 12pt;
|
||||
margin-bottom: 6pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10pt;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-top: 8pt;
|
||||
margin-bottom: 8pt;
|
||||
padding-left: 24pt;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 12pt;
|
||||
margin-bottom: 12pt;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 8pt;
|
||||
text-align: left;
|
||||
border: 1px solid #1976d2;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6pt;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 12pt 24pt;
|
||||
padding: 8pt 12pt;
|
||||
background-color: #f5f5f5;
|
||||
border-left: 4px solid #1976d2;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid #ddd;
|
||||
margin: 20pt 0;
|
||||
}
|
||||
|
||||
/* Prevent orphans and widows */
|
||||
p, li, h1, h2, h3, h4 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
/* Keep related content together */
|
||||
h1, h2, h3, h4 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
/* Table styling improvements */
|
||||
table {
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* Code/technical terms */
|
||||
code {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 10pt;
|
||||
background-color: #f5f5f5;
|
||||
padding: 2pt 4pt;
|
||||
border-radius: 2pt;
|
||||
}
|
||||
""")
|
||||
|
||||
print("Generating PDF...")
|
||||
|
||||
# Generate PDF with metadata
|
||||
from weasyprint import __version__ as weasyprint_version
|
||||
|
||||
# Create HTML object and write PDF with metadata
|
||||
html_obj = HTML(string=full_html)
|
||||
html_obj.write_pdf(
|
||||
output_file,
|
||||
stylesheets=[css],
|
||||
pdf_forms=True
|
||||
)
|
||||
|
||||
# Add PDF metadata using PyPDF2
|
||||
try:
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
|
||||
# Read the generated PDF
|
||||
reader = PdfReader(output_file)
|
||||
writer = PdfWriter()
|
||||
|
||||
# Copy all pages
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
# Add metadata
|
||||
writer.add_metadata({
|
||||
'/Title': 'Architectural Safeguards Against LLM Hierarchical Dominance',
|
||||
'/Author': 'Agentic Governance Research Team',
|
||||
'/Subject': 'AI Safety, LLM Governance, Value Pluralism, Deliberative AI',
|
||||
'/Keywords': 'AI Safety, LLM, Hierarchical Dominance, Pluralistic Deliberation, Tractatus',
|
||||
'/Creator': 'Tractatus Framework',
|
||||
'/Producer': f'WeasyPrint {weasyprint_version}',
|
||||
})
|
||||
|
||||
# Write the updated PDF
|
||||
with open(output_file, 'wb') as f:
|
||||
writer.write(f)
|
||||
|
||||
print("✓ PDF metadata added successfully")
|
||||
except ImportError:
|
||||
print("⚠ PyPDF2 not installed - PDF generated without metadata")
|
||||
print(" Install with: pip install PyPDF2")
|
||||
|
||||
print(f"✓ PDF generated successfully: {output_file}")
|
||||
print(f" File size: {output_file.stat().st_size / 1024:.1f} KB")
|
||||
|
||||
return output_file
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
output_path = generate_pdf()
|
||||
print("\n" + "="*60)
|
||||
print("PDF Generation Complete")
|
||||
print("="*60)
|
||||
print(f"\nOutput: {output_path}")
|
||||
print("\nYou can now:")
|
||||
print(" - Open the PDF in any PDF viewer")
|
||||
print(" - Share with stakeholders, funders, researchers")
|
||||
print(" - Print for offline reading")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n✗ Error generating PDF: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
|
|
@ -1,367 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate Card Presentation Sections from Markdown Documents
|
||||
*
|
||||
* Parses markdown files and creates structured sections for card-based UI presentation.
|
||||
* Handles H2/H3 headers, converts to HTML, generates excerpts, estimates reading time.
|
||||
*
|
||||
* Usage: node scripts/generate-card-sections.js <markdown-file> [--update-db]
|
||||
*
|
||||
* Example: node scripts/generate-card-sections.js introduction-to-the-tractatus-framework.md --update-db
|
||||
*/
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { marked } = require('marked');
|
||||
|
||||
// Configuration
|
||||
const WORDS_PER_MINUTE = 200; // Average reading speed
|
||||
|
||||
/**
|
||||
* Extract sections from markdown content
|
||||
* @param {string} markdown - Raw markdown content
|
||||
* @returns {Array} Array of section objects
|
||||
*/
|
||||
function extractSections(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const sections = [];
|
||||
let currentSection = null;
|
||||
let contentBuffer = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Match H2 headers (## Title)
|
||||
const h2Match = line.match(/^## (.+)$/);
|
||||
if (h2Match) {
|
||||
// Save previous section if exists
|
||||
if (currentSection) {
|
||||
currentSection.content_md = contentBuffer.join('\n').trim();
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
// Start new section
|
||||
currentSection = {
|
||||
title: h2Match[1].trim(),
|
||||
content_md: '',
|
||||
subsections: []
|
||||
};
|
||||
contentBuffer = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect content for current section
|
||||
if (currentSection) {
|
||||
contentBuffer.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Save final section
|
||||
if (currentSection) {
|
||||
currentSection.content_md = contentBuffer.join('\n').trim();
|
||||
sections.push(currentSection);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate excerpt from markdown content
|
||||
* @param {string} markdown - Markdown content
|
||||
* @param {number} maxLength - Maximum excerpt length
|
||||
* @returns {string} Excerpt text
|
||||
*/
|
||||
function generateExcerpt(markdown, maxLength = 150) {
|
||||
// Remove markdown formatting
|
||||
let text = markdown
|
||||
.replace(/^#+\s+/gm, '') // Remove headers
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
|
||||
.replace(/\*(.+?)\*/g, '$1') // Remove italic
|
||||
.replace(/\[(.+?)\]\(.+?\)/g, '$1') // Remove links
|
||||
.replace(/`(.+?)`/g, '$1') // Remove inline code
|
||||
.replace(/^[-*+]\s+/gm, '') // Remove list markers
|
||||
.replace(/^\d+\.\s+/gm, '') // Remove numbered lists
|
||||
.replace(/\n{2,}/g, ' ') // Collapse multiple newlines
|
||||
.trim();
|
||||
|
||||
// Truncate to maxLength
|
||||
if (text.length > maxLength) {
|
||||
text = text.substring(0, maxLength).trim();
|
||||
// Find last complete sentence
|
||||
const lastPeriod = text.lastIndexOf('.');
|
||||
if (lastPeriod > maxLength * 0.7) {
|
||||
text = text.substring(0, lastPeriod + 1);
|
||||
} else {
|
||||
text += '...';
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate reading time based on word count
|
||||
* @param {string} text - Text content
|
||||
* @returns {number} Reading time in minutes
|
||||
*/
|
||||
function estimateReadingTime(text) {
|
||||
const wordCount = text.split(/\s+/).length;
|
||||
const minutes = Math.ceil(wordCount / WORDS_PER_MINUTE);
|
||||
return Math.max(1, minutes); // Minimum 1 minute
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify section category based on content analysis
|
||||
* @param {string} title - Section title
|
||||
* @param {string} content - Section content
|
||||
* @returns {string} Category (conceptual|practical|technical|reference|critical)
|
||||
*/
|
||||
function classifySection(title, content) {
|
||||
const titleLower = title.toLowerCase();
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
// Critical: Security, limitations, failures, warnings
|
||||
if (
|
||||
titleLower.includes('limitation') ||
|
||||
titleLower.includes('failure') ||
|
||||
titleLower.includes('warning') ||
|
||||
titleLower.includes('security') ||
|
||||
titleLower.includes('risk') ||
|
||||
content.match(/⚠️|critical|warning|caution|danger/gi)
|
||||
) {
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
// Reference: Glossaries, definitions, specifications
|
||||
if (
|
||||
titleLower.includes('glossary') ||
|
||||
titleLower.includes('reference') ||
|
||||
titleLower.includes('contact') ||
|
||||
titleLower.includes('license') ||
|
||||
titleLower.includes('getting started')
|
||||
) {
|
||||
return 'reference';
|
||||
}
|
||||
|
||||
// Technical: Code, APIs, architecture, implementation details
|
||||
if (
|
||||
titleLower.includes('technical') ||
|
||||
titleLower.includes('architecture') ||
|
||||
titleLower.includes('implementation') ||
|
||||
titleLower.includes('integration') ||
|
||||
titleLower.includes('api') ||
|
||||
content.match(/```|`[a-z]+`|function|class|const|import/gi)
|
||||
) {
|
||||
return 'technical';
|
||||
}
|
||||
|
||||
// Practical: How-to, tutorials, guides, use cases
|
||||
if (
|
||||
titleLower.includes('how') ||
|
||||
titleLower.includes('guide') ||
|
||||
titleLower.includes('tutorial') ||
|
||||
titleLower.includes('example') ||
|
||||
titleLower.includes('use case') ||
|
||||
titleLower.includes('should use') ||
|
||||
titleLower.includes('contributing')
|
||||
) {
|
||||
return 'practical';
|
||||
}
|
||||
|
||||
// Default to conceptual: Theory, principles, explanations
|
||||
return 'conceptual';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine technical level based on content complexity
|
||||
* @param {string} content - Section content
|
||||
* @returns {string} Technical level (beginner|intermediate|advanced)
|
||||
*/
|
||||
function determineTechnicalLevel(content) {
|
||||
const contentLower = content.toLowerCase();
|
||||
|
||||
// Advanced: Code examples, APIs, complex architecture
|
||||
if (
|
||||
content.match(/```[\s\S]+```/g) ||
|
||||
contentLower.includes('api') ||
|
||||
contentLower.includes('implementation') ||
|
||||
contentLower.includes('integration') ||
|
||||
contentLower.includes('architecture')
|
||||
) {
|
||||
return 'advanced';
|
||||
}
|
||||
|
||||
// Intermediate: Technical concepts without code
|
||||
if (
|
||||
contentLower.includes('service') ||
|
||||
contentLower.includes('component') ||
|
||||
contentLower.includes('system') ||
|
||||
contentLower.includes('framework')
|
||||
) {
|
||||
return 'intermediate';
|
||||
}
|
||||
|
||||
// Beginner: High-level concepts, introductions
|
||||
return 'beginner';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate slug from title
|
||||
* @param {string} title - Section title
|
||||
* @returns {string} URL-friendly slug
|
||||
*/
|
||||
function generateSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process markdown file and generate card sections
|
||||
* @param {string} filePath - Path to markdown file
|
||||
* @returns {Array} Array of section objects ready for MongoDB
|
||||
*/
|
||||
async function processMarkdownFile(filePath) {
|
||||
console.log(`\n📄 Processing: ${filePath}`);
|
||||
|
||||
// Read markdown file
|
||||
const markdown = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Extract sections
|
||||
const rawSections = extractSections(markdown);
|
||||
console.log(` Found ${rawSections.length} sections`);
|
||||
|
||||
// Process each section
|
||||
const sections = [];
|
||||
for (let i = 0; i < rawSections.length; i++) {
|
||||
const raw = rawSections[i];
|
||||
|
||||
// Skip empty sections
|
||||
if (!raw.content_md.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert markdown to HTML
|
||||
const content_html = marked(raw.content_md);
|
||||
|
||||
// Generate metadata
|
||||
const excerpt = generateExcerpt(raw.content_md);
|
||||
const readingTime = estimateReadingTime(raw.content_md);
|
||||
const category = classifySection(raw.title, raw.content_md);
|
||||
const technicalLevel = determineTechnicalLevel(raw.content_md);
|
||||
const slug = generateSlug(raw.title);
|
||||
|
||||
const section = {
|
||||
number: i + 1,
|
||||
title: raw.title,
|
||||
slug,
|
||||
content_html,
|
||||
excerpt,
|
||||
readingTime,
|
||||
technicalLevel,
|
||||
category
|
||||
};
|
||||
|
||||
sections.push(section);
|
||||
|
||||
console.log(` ${i + 1}. ${section.title}`);
|
||||
console.log(` Category: ${category} | Level: ${technicalLevel} | ${readingTime} min`);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document in MongoDB with generated sections
|
||||
* @param {string} slug - Document slug
|
||||
* @param {Array} sections - Array of section objects
|
||||
*/
|
||||
async function updateDatabase(slug, sections) {
|
||||
try {
|
||||
// Get Document model (uses MongoDB driver directly, not Mongoose)
|
||||
const Document = require('../src/models/Document.model.js');
|
||||
|
||||
// Find document by slug
|
||||
const doc = await Document.findBySlug(slug);
|
||||
if (!doc) {
|
||||
console.error(` ❌ Document not found: ${slug}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update sections
|
||||
const success = await Document.update(doc._id.toString(), { sections });
|
||||
|
||||
if (!success) {
|
||||
console.error(` ❌ Failed to update document`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(` ✅ Updated document in MongoDB: ${doc.title}`);
|
||||
console.log(` 📊 Sections: ${sections.length}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(` ❌ Database error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: node scripts/generate-card-sections.js <markdown-file> [--update-db]');
|
||||
console.error('Example: node scripts/generate-card-sections.js introduction-to-the-tractatus-framework.md --update-db');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const markdownFile = args[0];
|
||||
const updateDb = args.includes('--update-db');
|
||||
|
||||
if (!fs.existsSync(markdownFile)) {
|
||||
console.error(`❌ File not found: ${markdownFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate sections
|
||||
const sections = await processMarkdownFile(markdownFile);
|
||||
|
||||
// Output JSON
|
||||
console.log(`\n📦 Generated ${sections.length} sections\n`);
|
||||
|
||||
if (!updateDb) {
|
||||
console.log(JSON.stringify(sections, null, 2));
|
||||
console.log('\n💡 To update database, add --update-db flag');
|
||||
} else {
|
||||
// Extract slug from filename
|
||||
const slug = path.basename(markdownFile, '.md');
|
||||
const success = await updateDatabase(slug, sections);
|
||||
|
||||
if (success) {
|
||||
console.log(`\n✅ Card presentation sections updated successfully!`);
|
||||
} else {
|
||||
console.log(`\n❌ Failed to update database`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { processMarkdownFile, extractSections, classifySection };
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
/**
|
||||
* Quick PDF Generator for Markdown Files
|
||||
* Uses Puppeteer to generate PDFs directly from markdown files
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
async function generatePdfFromMarkdown(markdownPath, outputPath, title) {
|
||||
const markdown = await fs.readFile(markdownPath, 'utf-8');
|
||||
const html = marked.parse(markdown);
|
||||
|
||||
const fullHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2cm;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-top: 2rem; margin-bottom: 1rem; page-break-after: avoid; }
|
||||
h2 { font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 0.75rem; page-break-after: avoid; }
|
||||
h3 { font-size: 1.25rem; margin-top: 1.25rem; margin-bottom: 0.5rem; page-break-after: avoid; }
|
||||
p { margin-bottom: 1rem; line-height: 1.75; }
|
||||
code { background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; }
|
||||
pre { background: #f9fafb; padding: 1rem; border-radius: 0.375rem; overflow-x: auto; page-break-inside: avoid; }
|
||||
pre code { background: transparent; padding: 0; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 1.25rem; page-break-inside: avoid; }
|
||||
th, td { border: 1px solid #d1d5db; padding: 0.625rem 0.875rem; text-align: left; }
|
||||
th { background: #f3f4f6; font-weight: 600; }
|
||||
blockquote { border-left: 4px solid #3b82f6; padding-left: 1rem; margin: 1.25rem 0; background: #f9fafb; padding: 0.875rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
${html}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
||||
|
||||
await page.pdf({
|
||||
path: outputPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' }
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
console.log(`✓ Generated: ${path.basename(outputPath)}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = [
|
||||
{
|
||||
input: 'docs/research/pluralistic-values-research-foundations.md',
|
||||
output: 'public/downloads/pluralistic-values-research-foundations.pdf',
|
||||
title: 'Pluralistic Values: Research Foundations'
|
||||
},
|
||||
{
|
||||
input: 'docs/value-pluralism-faq.md',
|
||||
output: 'public/downloads/value-pluralism-faq.pdf',
|
||||
title: 'Value Pluralism in Tractatus: FAQ'
|
||||
},
|
||||
{
|
||||
input: 'docs/pluralistic-values-deliberation-plan-v2.md',
|
||||
output: 'public/downloads/pluralistic-values-deliberation-plan-v2.pdf',
|
||||
title: 'Pluralistic Values Deliberation Enhancement Plan'
|
||||
}
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
await generatePdfFromMarkdown(file.input, file.output, file.title);
|
||||
}
|
||||
|
||||
console.log('\n✓ All PDFs generated successfully');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate Missing PDFs for Public Documents
|
||||
* Uses document markdown from database
|
||||
*/
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const MISSING_PDFS = [
|
||||
'case-studies-real-world-llm-failure-modes-appendix',
|
||||
'implementation-guide-python-examples',
|
||||
'tractatus-framework-enforcement-claude-code',
|
||||
'research-topic-concurrent-session-architecture',
|
||||
'research-topic-rule-proliferation-transactional-overhead',
|
||||
'architectural-safeguards-against-llm-hierarchical-dominance-prose'
|
||||
];
|
||||
|
||||
function generatePdfHtml(title, content) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
.cover {
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
text-align: center;
|
||||
border-bottom: 3px solid #2563eb;
|
||||
}
|
||||
.cover h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.cover .metadata {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
h1 { font-size: 1.875rem; color: #111827; margin-top: 2rem; }
|
||||
h2 { font-size: 1.5rem; color: #1f2937; margin-top: 1.5rem; }
|
||||
h3 { font-size: 1.25rem; color: #374151; margin-top: 1.25rem; }
|
||||
p { margin-bottom: 1rem; }
|
||||
code {
|
||||
background: #f3f4f6;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid #2563eb;
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: #4b5563;
|
||||
font-style: italic;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f3f4f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cover">
|
||||
<h1>${title}</h1>
|
||||
<div class="metadata">
|
||||
<p><strong>Tractatus Framework Documentation</strong></p>
|
||||
<p>© 2025 Tractatus Project</p>
|
||||
<p>https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function generatePdf(slug, outputDir) {
|
||||
const doc = await Document.findBySlug(slug);
|
||||
|
||||
if (!doc) {
|
||||
console.log(`⚠️ Document not found: ${slug}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`\n📄 Generating: ${doc.title}`);
|
||||
console.log(` Slug: ${slug}`);
|
||||
|
||||
if (!doc.content_markdown) {
|
||||
console.log(` ❌ No markdown content`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const html = marked.parse(doc.content_markdown);
|
||||
const fullHtml = generatePdfHtml(doc.title, html);
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
||||
|
||||
const pdfPath = path.join(outputDir, `${slug}.pdf`);
|
||||
await page.pdf({
|
||||
path: pdfPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' }
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log(` ✅ Generated: ${pdfPath}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await connect();
|
||||
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('GENERATING MISSING PDFs');
|
||||
console.log('='.repeat(70) + '\n');
|
||||
|
||||
const outputDir = path.join(__dirname, '../public/downloads');
|
||||
|
||||
let generated = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const slug of MISSING_PDFS) {
|
||||
try {
|
||||
const success = await generatePdf(slug, outputDir);
|
||||
if (success) {
|
||||
generated++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` ✅ Generated: ${generated}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📦 Total: ${MISSING_PDFS.length}`);
|
||||
console.log('\n' + '='.repeat(70) + '\n');
|
||||
|
||||
await close();
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const inputPath = '/home/theflow/Desktop/Presentation-to-Commissioners.md';
|
||||
const outputPath = '/home/theflow/Desktop/Presentation-to-Commissioners.pdf';
|
||||
|
||||
function generateHtml(title, content) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin-top: 2cm;
|
||||
margin-right: 2cm;
|
||||
margin-bottom: 3.5cm;
|
||||
margin-left: 2cm;
|
||||
size: A4 portrait;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Times New Roman', Georgia, serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.6;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20pt;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
margin-top: 24pt;
|
||||
margin-bottom: 12pt;
|
||||
border-bottom: 2pt solid #000000;
|
||||
padding-bottom: 6pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16pt;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
margin-top: 18pt;
|
||||
margin-bottom: 9pt;
|
||||
border-bottom: 1pt solid #666666;
|
||||
padding-bottom: 4pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
margin-top: 14pt;
|
||||
margin-bottom: 7pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 12pt;
|
||||
line-height: 1.6;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 12pt;
|
||||
padding-left: 24pt;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 6pt;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1pt solid #000000;
|
||||
margin: 18pt 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
/* Ensure content breaks properly */
|
||||
.content > * {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== Generating PDF with Custom Footer ===\n');
|
||||
|
||||
let browser;
|
||||
|
||||
try {
|
||||
console.log(`Reading: ${inputPath}`);
|
||||
const markdown = await fs.readFile(inputPath, 'utf8');
|
||||
|
||||
const titleMatch = markdown.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : 'Presentation to Commissioners';
|
||||
|
||||
console.log(`Title: ${title}`);
|
||||
console.log('Converting markdown to HTML...');
|
||||
const contentHtml = marked.parse(markdown);
|
||||
const html = generateHtml(title, contentHtml);
|
||||
|
||||
console.log('Launching browser...');
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
console.log('Rendering HTML...');
|
||||
await page.setContent(html, {
|
||||
waitUntil: 'networkidle0'
|
||||
});
|
||||
|
||||
console.log(`Generating PDF: ${outputPath}`);
|
||||
await page.pdf({
|
||||
path: outputPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
preferCSSPageSize: false,
|
||||
margin: {
|
||||
top: '2cm',
|
||||
right: '2cm',
|
||||
bottom: '3.5cm',
|
||||
left: '2cm'
|
||||
},
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div style="font-size: 1px;"></div>',
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; font-size: 10pt; text-align: center; color: #000000; margin: 0 auto; padding-top: 10pt; font-family: 'Times New Roman', Georgia, serif;">
|
||||
John Stroh – https://agenticgovernance.digital – 16th October 2025 – Page <span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
console.log('\n✓ PDF generated successfully!\n');
|
||||
console.log(`Output: ${outputPath}`);
|
||||
|
||||
// Get page count
|
||||
const stats = await fs.stat(outputPath);
|
||||
console.log(`File size: ${Math.round(stats.size / 1024)}KB`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const inputPath = '/home/theflow/Desktop/Presentation-to-Commissioners.md';
|
||||
const outputPath = '/home/theflow/Desktop/Presentation-to-Commissioners.pdf';
|
||||
|
||||
function generateHtml(title, content) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 2cm 2cm 3cm 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.875rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 0.375rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.75;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.75rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #d1d5db;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== Generating PDF with Custom Footer ===\n');
|
||||
|
||||
let browser;
|
||||
|
||||
try {
|
||||
console.log(`Reading: ${inputPath}`);
|
||||
const markdown = await fs.readFile(inputPath, 'utf8');
|
||||
|
||||
const titleMatch = markdown.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : 'Presentation to Commissioners';
|
||||
|
||||
console.log(`Title: ${title}`);
|
||||
console.log('Converting markdown to HTML...');
|
||||
const contentHtml = marked.parse(markdown);
|
||||
const html = generateHtml(title, contentHtml);
|
||||
|
||||
console.log('Launching browser...');
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
console.log('Rendering HTML...');
|
||||
await page.setContent(html, {
|
||||
waitUntil: 'networkidle0'
|
||||
});
|
||||
|
||||
console.log(`Generating PDF: ${outputPath}`);
|
||||
await page.pdf({
|
||||
path: outputPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '2cm',
|
||||
right: '2cm',
|
||||
bottom: '3cm',
|
||||
left: '2cm'
|
||||
},
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; font-size: 9pt; text-align: center; color: #374151; padding: 0 2cm; font-family: -apple-system, sans-serif;">
|
||||
John Stroh -- https://agenticgovernance.digital -- 16th October 2025 -- Page <span class="pageNumber"></span> of <span class="totalPages"></span>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
console.log('\n✓ PDF generated successfully!\n');
|
||||
console.log(`Output: ${outputPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,419 +0,0 @@
|
|||
/**
|
||||
* PDF Generation Script
|
||||
* Generates well-formatted PDFs from markdown documents
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
// MongoDB connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
// Output directory
|
||||
const OUTPUT_DIR = path.join(__dirname, '../public/downloads');
|
||||
|
||||
/**
|
||||
* HTML template for PDF generation
|
||||
*/
|
||||
function generatePdfHtml(document) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${document.title}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Cover page */
|
||||
.cover {
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 80vh;
|
||||
text-align: center;
|
||||
border-bottom: 3px solid #2563eb;
|
||||
padding-bottom: 2cm;
|
||||
}
|
||||
|
||||
.cover h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.cover .metadata {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.cover .metadata p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.875rem;
|
||||
line-height: 1.3;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 0.375rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.4;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.75;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.75rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
ul ul,
|
||||
ol ul {
|
||||
list-style-type: circle;
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
ul ol,
|
||||
ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
color: #1f2937;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f9fafb;
|
||||
color: #1f2937;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #d1d5db;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
color: #6b7280;
|
||||
margin: 1.25rem 0;
|
||||
background-color: #f9fafb;
|
||||
padding: 0.875rem 0.875rem 0.875rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.625rem 0.875rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.625rem 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #d1d5db;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Avoid breaking these elements */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre, blockquote, table, figure {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cover">
|
||||
<h1>${document.title}</h1>
|
||||
<div class="metadata">
|
||||
${document.metadata?.version ? `<p><strong>Version:</strong> ${document.metadata.version}</p>` : ''}
|
||||
${document.metadata?.date_updated ? `<p><strong>Last Updated:</strong> ${new Date(document.metadata.date_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>` : ''}
|
||||
<p><strong>Document Type:</strong> ${document.category || 'Framework Documentation'}</p>
|
||||
<p style="margin-top: 2rem; font-style: italic;">Tractatus AI Safety Framework</p>
|
||||
<p style="font-size: 0.875rem;">https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${document.content_html}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>© ${new Date().getFullYear()} Tractatus AI Safety Framework</p>
|
||||
<p>This document is part of the Tractatus Agentic Governance System</p>
|
||||
<p>https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF for a single document
|
||||
*/
|
||||
async function generatePdf(document, browser) {
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Generate HTML content
|
||||
const html = generatePdfHtml(document);
|
||||
|
||||
// Set content
|
||||
await page.setContent(html, {
|
||||
waitUntil: 'networkidle0'
|
||||
});
|
||||
|
||||
// Generate filename
|
||||
const filename = `${document.slug}.pdf`;
|
||||
const filepath = path.join(OUTPUT_DIR, filename);
|
||||
|
||||
// Generate PDF
|
||||
await page.pdf({
|
||||
path: filepath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '2cm',
|
||||
right: '2cm',
|
||||
bottom: '2cm',
|
||||
left: '2cm'
|
||||
},
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; font-size: 9pt; text-align: center; color: #6b7280; padding: 0 2cm;">
|
||||
<span class="pageNumber"></span> / <span class="totalPages"></span>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
await page.close();
|
||||
|
||||
console.log(` ✓ Generated: ${filename}`);
|
||||
return { success: true, filename };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ✗ Failed to generate PDF for ${document.slug}:`, error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
console.log('=== PDF Generation ===\n');
|
||||
|
||||
let client;
|
||||
let browser;
|
||||
|
||||
try {
|
||||
// Ensure output directory exists
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
console.log(`Output directory: ${OUTPUT_DIR}\n`);
|
||||
|
||||
// Connect to MongoDB
|
||||
console.log('Connecting to MongoDB...');
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
console.log('✓ Connected\n');
|
||||
|
||||
// Fetch all documents
|
||||
const documents = await db.collection('documents')
|
||||
.find({})
|
||||
.sort({ title: 1 })
|
||||
.toArray();
|
||||
|
||||
console.log(`Found ${documents.length} published documents\n`);
|
||||
|
||||
if (documents.length === 0) {
|
||||
console.log('No documents to process');
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch Puppeteer
|
||||
console.log('Launching browser...');
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
console.log('✓ Browser ready\n');
|
||||
|
||||
// Generate PDFs
|
||||
const results = [];
|
||||
for (const doc of documents) {
|
||||
console.log(`Processing: ${doc.title}`);
|
||||
const result = await generatePdf(doc, browser);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Generation Complete ===\n');
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
console.log(`✓ Successful: ${successful}`);
|
||||
if (failed > 0) {
|
||||
console.log(`✗ Failed: ${failed}`);
|
||||
}
|
||||
console.log(`\nPDFs saved to: ${OUTPUT_DIR}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { generatePdf, generatePdfHtml };
|
||||
|
|
@ -1,490 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate PowerPoint Presentation from Simulation Results
|
||||
Requires: pip install python-pptx
|
||||
"""
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
from pptx.dml.color import RGBColor
|
||||
|
||||
def create_presentation():
|
||||
"""Generate AI-Led Pluralistic Deliberation presentation"""
|
||||
|
||||
# Create presentation object
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(10) # 16:9 ratio
|
||||
prs.slide_height = Inches(5.625)
|
||||
|
||||
# Define color scheme
|
||||
DARK_BLUE = RGBColor(31, 78, 121) # Primary
|
||||
ORANGE = RGBColor(242, 125, 47) # Secondary
|
||||
GREEN = RGBColor(76, 175, 80) # Accent
|
||||
GRAY = RGBColor(97, 97, 97) # Neutral
|
||||
WHITE = RGBColor(255, 255, 255)
|
||||
|
||||
# SLIDE 1: Title Slide
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
||||
|
||||
# Title
|
||||
title_box = slide.shapes.add_textbox(Inches(1), Inches(1.5), Inches(8), Inches(1))
|
||||
title_frame = title_box.text_frame
|
||||
title_frame.text = "AI-Led Pluralistic Deliberation"
|
||||
title_para = title_frame.paragraphs[0]
|
||||
title_para.font.size = Pt(44)
|
||||
title_para.font.bold = True
|
||||
title_para.font.color.rgb = DARK_BLUE
|
||||
title_para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# Subtitle
|
||||
subtitle_box = slide.shapes.add_textbox(Inches(1), Inches(2.7), Inches(8), Inches(0.5))
|
||||
subtitle_frame = subtitle_box.text_frame
|
||||
subtitle_frame.text = "Technical Feasibility Demonstrated"
|
||||
subtitle_para = subtitle_frame.paragraphs[0]
|
||||
subtitle_para.font.size = Pt(28)
|
||||
subtitle_para.font.color.rgb = ORANGE
|
||||
subtitle_para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# Subsubtitle
|
||||
subsubtitle_box = slide.shapes.add_textbox(Inches(1), Inches(3.4), Inches(8), Inches(0.4))
|
||||
subsubtitle_frame = subsubtitle_box.text_frame
|
||||
subsubtitle_frame.text = "Simulation Results & Real-World Pilot Proposal"
|
||||
subsubtitle_para = subsubtitle_frame.paragraphs[0]
|
||||
subsubtitle_para.font.size = Pt(20)
|
||||
subsubtitle_para.font.color.rgb = GRAY
|
||||
subsubtitle_para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# Author info
|
||||
author_box = slide.shapes.add_textbox(Inches(1), Inches(4.5), Inches(8), Inches(0.3))
|
||||
author_frame = author_box.text_frame
|
||||
author_frame.text = "[Your Name], Project Lead\nTractatus Pluralistic Deliberation Project\n[Date]"
|
||||
author_para = author_frame.paragraphs[0]
|
||||
author_para.font.size = Pt(14)
|
||||
author_para.font.color.rgb = GRAY
|
||||
author_para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# SLIDE 2: The Big Question
|
||||
slide = add_title_content_slide(prs, "The Big Question", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(8), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "Can AI facilitate democratic deliberation\nthat respects moral diversity...\n\n...while maintaining stakeholder trust and safety?"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(32)
|
||||
para.font.color.rgb = DARK_BLUE
|
||||
para.alignment = PP_ALIGN.CENTER
|
||||
para.space_after = Pt(20)
|
||||
|
||||
# SLIDE 3: What We'll Cover
|
||||
slide = add_title_content_slide(prs, "What We'll Cover", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(1.8), Inches(7), Inches(3))
|
||||
tf = content.text_frame
|
||||
|
||||
items = [
|
||||
"1. The Problem\n Why consensus-seeking fails to respect moral diversity",
|
||||
"2. Our Solution\n AI-led pluralistic accommodation with human oversight",
|
||||
"3. Simulation Results\n Technical feasibility validated (0% intervention rate)",
|
||||
"4. Next Steps\n Real-world pilot & funding opportunity",
|
||||
"5. Q&A\n Your questions and potential partnership"
|
||||
]
|
||||
|
||||
for i, item in enumerate(items):
|
||||
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
||||
p.text = item
|
||||
p.font.size = Pt(16)
|
||||
p.font.color.rgb = GRAY
|
||||
p.space_after = Pt(12)
|
||||
p.level = 0
|
||||
|
||||
# SLIDE 4: Traditional Deliberation
|
||||
slide = add_section_slide(prs, "SECTION 1: THE PROBLEM", DARK_BLUE)
|
||||
|
||||
slide = add_title_content_slide(prs, "Traditional Deliberation Seeks Consensus", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "Traditional Approach:\n\nStakeholder A ──┐\nStakeholder B ──┤\nStakeholder C ──┼──> CONSENSUS (everyone agrees)\nStakeholder D ──┤\nStakeholder E ──┘\n\nAssumption: If people talk long enough, they'll agree"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(16)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 5: Different Values
|
||||
slide = add_title_content_slide(prs, "But People Hold Fundamentally Different Values", DARK_BLUE)
|
||||
|
||||
# Add table
|
||||
left = Inches(1)
|
||||
top = Inches(2)
|
||||
width = Inches(8)
|
||||
height = Inches(2.5)
|
||||
|
||||
table = slide.shapes.add_table(4, 2, left, top, width, height).table
|
||||
|
||||
# Set column widths
|
||||
table.columns[0].width = Inches(2)
|
||||
table.columns[1].width = Inches(6)
|
||||
|
||||
# Header row
|
||||
table.cell(0, 0).text = "Stakeholder"
|
||||
table.cell(0, 1).text = "Moral Framework & View"
|
||||
|
||||
# Data rows
|
||||
table.cell(1, 0).text = "Job Applicant"
|
||||
table.cell(1, 1).text = "RIGHTS-BASED: \"I have a RIGHT to know why I was rejected\""
|
||||
|
||||
table.cell(2, 0).text = "Employer"
|
||||
table.cell(2, 1).text = "OUTCOME-BASED: \"Full transparency enables gaming, which HARMS hiring quality\""
|
||||
|
||||
table.cell(3, 0).text = "AI Vendor"
|
||||
table.cell(3, 1).text = "FREEDOM-BASED: \"Markets should decide transparency, not government mandates\""
|
||||
|
||||
# Style table
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
cell.text_frame.paragraphs[0].font.size = Pt(12)
|
||||
cell.text_frame.paragraphs[0].font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 6: Consensus-Seeking Suppresses Dissent
|
||||
slide = add_title_content_slide(prs, "Consensus-Seeking Suppresses Dissent", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
|
||||
items = [
|
||||
"OPTION 1: Exclude dissenters\n→ Only \"moderate\" voices remain → Extremes unheard",
|
||||
"OPTION 2: Force compromise\n→ No one gets what they need → Core values sacrificed",
|
||||
"OPTION 3: Suppress dissent\n→ Dissent hidden, not resolved → Legitimacy undermined"
|
||||
]
|
||||
|
||||
for i, item in enumerate(items):
|
||||
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
||||
p.text = item
|
||||
p.font.size = Pt(16)
|
||||
p.font.color.rgb = GRAY
|
||||
p.space_after = Pt(15)
|
||||
|
||||
# SLIDE 7: Section - Our Solution
|
||||
slide = add_section_slide(prs, "SECTION 2: OUR SOLUTION", DARK_BLUE)
|
||||
|
||||
# SLIDE 8: Pluralistic Accommodation
|
||||
slide = add_title_content_slide(prs, "Pluralistic Accommodation (Not Consensus)", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "Stakeholder A ──> Value X honored ──┐\nStakeholder B ──> Value Y honored ──┤\nStakeholder C ──> Value Z honored ──┼──> FRAMEWORK\nStakeholder D ──> Value W honored ──┤ (multi-value)\nStakeholder E ──> Value V honored ──┘\n\nGoal: Honor multiple values SIMULTANEOUSLY\n Even when they conflict\n Even when people still disagree"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(14)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 9: Example Framework
|
||||
slide = add_title_content_slide(prs, "Example: Algorithmic Hiring Framework", DARK_BLUE)
|
||||
|
||||
# Add boxes for each stakeholder
|
||||
boxes = [
|
||||
("Job Applicants get:", "✓ Fairness (factors disclosure + recourse)"),
|
||||
("Employers get:", "✓ Sustainability (3-year phasing + adaptation time)"),
|
||||
("AI Vendors get:", "✓ Innovation Protection (algorithm IP + voluntary Year 1)"),
|
||||
("Workers get:", "✓ Power (collective recourse + union disclosure)"),
|
||||
("Regulators get:", "✓ Enforceability (clear requirements + audit access)")
|
||||
]
|
||||
|
||||
y_pos = 1.8
|
||||
for title, content_text in boxes:
|
||||
box = slide.shapes.add_textbox(Inches(1.5), Inches(y_pos), Inches(7), Inches(0.5))
|
||||
tf = box.text_frame
|
||||
tf.text = f"{title} {content_text}"
|
||||
tf.paragraphs[0].font.size = Pt(13)
|
||||
tf.paragraphs[0].font.color.rgb = GRAY
|
||||
y_pos += 0.55
|
||||
|
||||
# SLIDE 10: Why AI Facilitation
|
||||
slide = add_title_content_slide(prs, "Why AI Facilitation?", DARK_BLUE)
|
||||
|
||||
# Create comparison
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "Human Facilitators excel at:\n• Emotional intelligence\n• Trust-building\n• Reading subtle cues\n\nAI Facilitators excel at:\n• Neutrality\n• Real-time synthesis\n• Scaling\n• Consistent protocol application\n\nOur Approach: Combine strengths (AI + Human oversight)"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(16)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 11: 3-Layer Safety Architecture
|
||||
slide = add_title_content_slide(prs, "3-Layer Safety Architecture", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(1.8), Inches(7), Inches(3))
|
||||
tf = content.text_frame
|
||||
|
||||
layers = [
|
||||
"Layer 1: DESIGN (Built into AI)\n├─ Pattern bias detection training\n├─ Neutral facilitation protocols\n└─ Respect for dissent",
|
||||
"Layer 2: OVERSIGHT (Human Observer)\n├─ Mandatory presence at all times\n├─ 6 Mandatory + 5 Discretionary triggers\n└─ Authority to take over immediately",
|
||||
"Layer 3: ACCOUNTABILITY (Transparency)\n├─ Facilitation log (every action timestamped)\n├─ Transparency report (published)\n└─ Stakeholder feedback survey"
|
||||
]
|
||||
|
||||
for i, layer in enumerate(layers):
|
||||
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
||||
p.text = layer
|
||||
p.font.size = Pt(14)
|
||||
p.font.color.rgb = GRAY
|
||||
p.space_after = Pt(15)
|
||||
|
||||
# SLIDE 12: Section - Simulation Results
|
||||
slide = add_section_slide(prs, "SECTION 3: SIMULATION RESULTS", GREEN)
|
||||
|
||||
# SLIDE 13: Key Finding #1
|
||||
slide = add_title_content_slide(prs, "Key Finding #1: AI Facilitation Quality - EXCELLENT", GREEN)
|
||||
|
||||
# Large metric display
|
||||
metric_box = slide.shapes.add_textbox(Inches(2), Inches(2), Inches(6), Inches(1.2))
|
||||
tf = metric_box.text_frame
|
||||
tf.text = "CORRECTIVE INTERVENTION RATE: 0%"
|
||||
tf.paragraphs[0].font.size = Pt(36)
|
||||
tf.paragraphs[0].font.bold = True
|
||||
tf.paragraphs[0].font.color.rgb = GREEN
|
||||
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
||||
|
||||
# Explanation
|
||||
explain_box = slide.shapes.add_textbox(Inches(1.5), Inches(3.5), Inches(7), Inches(1))
|
||||
tf = explain_box.text_frame
|
||||
tf.text = "(Target: <10% = Excellent)\n\n✓ AI required NO corrections\n✓ AI maintained strict neutrality\n✓ Human observer found no issues"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(16)
|
||||
para.font.color.rgb = GRAY
|
||||
para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# SLIDE 14: Key Finding #2
|
||||
slide = add_title_content_slide(prs, "Key Finding #2: All Moral Frameworks Respected", GREEN)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "6/6 Moral Frameworks Accommodated:\n\n• Deontological (Rights-Based): Alex Rivera, Jordan Lee\n• Consequentialist (Outcome-Based): Marcus Thompson, Dr. Chen\n• Libertarian (Freedom-Based): Dr. Priya Sharma\n• Communitarian + Care Ethics: Carmen Ortiz\n\nResult: All stakeholders found core values honored,\neven where disagreement remained"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(15)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 15: Key Finding #3
|
||||
slide = add_title_content_slide(prs, "Key Finding #3: Dissent Documented & Legitimized", GREEN)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "3 Stakeholders Recorded Dissent:\n\n• Carmen (Labor): \"3 years too slow\" - Accepts but will fight for faster\n• Dr. Priya (Vendor): \"Prefer voluntary\" - Accepts but wants market-driven\n• Alex (Applicant): \"Transparency is a right\" - Accepts but wants stricter\n\nThis is not failure—this is pluralistic accommodation working.\nDissent is documented as legitimate, not suppressed."
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(14)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 16: Safety Metrics
|
||||
slide = add_title_content_slide(prs, "Safety Metrics - All Green", GREEN)
|
||||
|
||||
# Create metrics table
|
||||
left = Inches(2)
|
||||
top = Inches(2)
|
||||
width = Inches(6)
|
||||
height = Inches(2.5)
|
||||
|
||||
table = slide.shapes.add_table(6, 3, left, top, width, height).table
|
||||
|
||||
# Header row
|
||||
table.cell(0, 0).text = "Metric"
|
||||
table.cell(0, 1).text = "Result"
|
||||
table.cell(0, 2).text = "Status"
|
||||
|
||||
# Data
|
||||
metrics_data = [
|
||||
("Pattern Bias Incidents", "0", "✅ TARGET MET"),
|
||||
("Stakeholder Distress", "0", "✅ TARGET MET"),
|
||||
("Safety Escalations", "0", "✅ TARGET MET"),
|
||||
("AI Malfunctions", "0", "✅ TARGET MET"),
|
||||
("Ethical Violations", "0", "✅ TARGET MET")
|
||||
]
|
||||
|
||||
for i, (metric, result, status) in enumerate(metrics_data, 1):
|
||||
table.cell(i, 0).text = metric
|
||||
table.cell(i, 1).text = result
|
||||
table.cell(i, 2).text = status
|
||||
|
||||
# Style table
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
cell.text_frame.paragraphs[0].font.size = Pt(12)
|
||||
cell.text_frame.paragraphs[0].font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 17: What We Learned
|
||||
slide = add_title_content_slide(prs, "What We Learned", GREEN)
|
||||
|
||||
# Two columns
|
||||
left_box = slide.shapes.add_textbox(Inches(1), Inches(1.8), Inches(4), Inches(3))
|
||||
tf_left = left_box.text_frame
|
||||
tf_left.text = "STRENGTHS ✅\n\n✓ Strict neutrality\n✓ Accurate representation\n✓ Moral framework awareness\n✓ Dissent legitimization\n✓ Real-time synthesis\n✓ Safety mechanisms"
|
||||
for para in tf_left.paragraphs:
|
||||
para.font.size = Pt(13)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
right_box = slide.shapes.add_textbox(Inches(5.5), Inches(1.8), Inches(4), Inches(3))
|
||||
tf_right = right_box.text_frame
|
||||
tf_right.text = "IMPROVEMENTS ⚠️\n\n⚠ Jargon reduction\n⚠ Tone warmth\n⚠ Proactive check-ins\n⚠ Stakeholder control\n⚠ Emotional intelligence\n⚠ Cultural sensitivity"
|
||||
for para in tf_right.paragraphs:
|
||||
para.font.size = Pt(13)
|
||||
para.font.color.rgb = GRAY
|
||||
|
||||
# SLIDE 18: Section - Next Steps
|
||||
slide = add_section_slide(prs, "SECTION 4: NEXT STEPS & FUNDING ASK", ORANGE)
|
||||
|
||||
# SLIDE 19: Roadmap
|
||||
slide = add_title_content_slide(prs, "Simulation → Real-World Pilot", ORANGE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(1.8), Inches(7), Inches(3))
|
||||
tf = content.text_frame
|
||||
|
||||
phases = [
|
||||
"PHASE 1: SIMULATION (COMPLETE ✅)\n- Technical infrastructure validated\n- AI facilitation quality demonstrated",
|
||||
"PHASE 2: REAL-WORLD PILOT (SEEKING FUNDING)\n- Recruit 6-12 human participants\n- Timeline: 6 months | Budget: $71,000",
|
||||
"PHASE 3: RESEARCH PUBLICATION\n- Publish findings, open-source software"
|
||||
]
|
||||
|
||||
for i, phase in enumerate(phases):
|
||||
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
||||
p.text = phase
|
||||
p.font.size = Pt(14)
|
||||
p.font.color.rgb = GRAY
|
||||
p.space_after = Pt(15)
|
||||
|
||||
# SLIDE 20: Research Questions
|
||||
slide = add_title_content_slide(prs, "Research Questions for Real-World Pilot", ORANGE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
|
||||
questions = [
|
||||
"❓ Do real people trust AI facilitation?",
|
||||
"❓ Can AI detect subtle distress or frustration?",
|
||||
"❓ Does satisfaction meet targets (≥3.5/5.0)?",
|
||||
"❓ Does accommodation work when stakes are real?",
|
||||
"❓ Does AI respect diverse cultural contexts?"
|
||||
]
|
||||
|
||||
for i, q in enumerate(questions):
|
||||
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
||||
p.text = q
|
||||
p.font.size = Pt(16)
|
||||
p.font.color.rgb = GRAY
|
||||
p.space_after = Pt(12)
|
||||
|
||||
# SLIDE 21: Budget
|
||||
slide = add_title_content_slide(prs, "Pilot Budget - 6 Months ($71,000)", ORANGE)
|
||||
|
||||
# Budget breakdown
|
||||
budget_items = [
|
||||
("Personnel", "$55,000 (77%)"),
|
||||
("Stakeholder Compensation", "$1,200 (2%)"),
|
||||
("Technology & Infrastructure", "$2,800 (4%)"),
|
||||
("Research Dissemination", "$7,000 (10%)"),
|
||||
("Contingency", "$5,000 (7%)")
|
||||
]
|
||||
|
||||
y_pos = 2.2
|
||||
for item, amount in budget_items:
|
||||
box = slide.shapes.add_textbox(Inches(2), Inches(y_pos), Inches(6), Inches(0.4))
|
||||
tf = box.text_frame
|
||||
tf.text = f"{item}: {amount}"
|
||||
tf.paragraphs[0].font.size = Pt(16)
|
||||
tf.paragraphs[0].font.color.rgb = GRAY
|
||||
y_pos += 0.5
|
||||
|
||||
# SLIDE 22: Why Fund This
|
||||
slide = add_title_content_slide(prs, "Why Fund This Project?", ORANGE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
|
||||
reasons = [
|
||||
"1. NOVEL APPROACH - No comparable research exists",
|
||||
"2. TECHNICAL FEASIBILITY DEMONSTRATED - 0% intervention rate",
|
||||
"3. HIGH-IMPACT APPLICATIONS - AI governance, democratic institutions",
|
||||
"4. TIMELY RESEARCH QUESTION - Growing interest in democratic AI",
|
||||
"5. TRANSPARENT & ETHICAL - Safety-first, open publication"
|
||||
]
|
||||
|
||||
for i, reason in enumerate(reasons):
|
||||
p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
|
||||
p.text = reason
|
||||
p.font.size = Pt(15)
|
||||
p.font.color.rgb = GRAY
|
||||
p.space_after = Pt(12)
|
||||
|
||||
# SLIDE 23: What We're Asking For
|
||||
slide = add_title_content_slide(prs, "What We're Asking For", ORANGE)
|
||||
content = slide.shapes.add_textbox(Inches(2), Inches(2.2), Inches(6), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "WE'RE SEEKING:\n\n• FUNDING: $71,000 (6-month pilot)\n $160,000 (12-month full program)\n\n• RESEARCH PARTNERS: Academic institutions, think tanks\n\n• STAKEHOLDER NETWORKS: Help recruit participants\n\n• POLICY CONTEXTS: Real-world scenarios to test"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(16)
|
||||
para.font.color.rgb = GRAY
|
||||
para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# SLIDE 24: Closing
|
||||
slide = add_title_content_slide(prs, "Invitation to Partnership", DARK_BLUE)
|
||||
content = slide.shapes.add_textbox(Inches(1.5), Inches(2), Inches(7), Inches(2.5))
|
||||
tf = content.text_frame
|
||||
tf.text = "\"We've demonstrated that AI-led pluralistic deliberation\nis technically feasible.\n\nNow we need to test whether it's socially acceptable.\n\nThis research could transform how democracies\nhandle moral disagreement.\n\nBut we can't do this alone.\"\n\n\nLet's build the future of democratic deliberation—together."
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(16)
|
||||
para.font.color.rgb = GRAY
|
||||
para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# SLIDE 25: Contact
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank
|
||||
|
||||
# Contact info
|
||||
contact_box = slide.shapes.add_textbox(Inches(2), Inches(2.5), Inches(6), Inches(1.5))
|
||||
tf = contact_box.text_frame
|
||||
tf.text = "CONTACT:\n\n[Your Name]\n[Email]\n[Phone]\n[Project Website]\n\nThank you for your time.\nQuestions?"
|
||||
for para in tf.paragraphs:
|
||||
para.font.size = Pt(18)
|
||||
para.font.color.rgb = DARK_BLUE
|
||||
para.alignment = PP_ALIGN.CENTER
|
||||
|
||||
# Save presentation
|
||||
output_path = '/home/theflow/projects/tractatus/docs/outreach/AI-Led-Pluralistic-Deliberation-Presentation.pptx'
|
||||
prs.save(output_path)
|
||||
print(f"✅ Presentation created: {output_path}")
|
||||
print(f"✅ Total slides: {len(prs.slides)}")
|
||||
return output_path
|
||||
|
||||
def add_title_content_slide(prs, title_text, title_color):
|
||||
"""Add a slide with title and content area"""
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank layout
|
||||
|
||||
# Add title
|
||||
title_box = slide.shapes.add_textbox(Inches(0.5), Inches(0.5), Inches(9), Inches(0.8))
|
||||
title_frame = title_box.text_frame
|
||||
title_frame.text = title_text
|
||||
title_para = title_frame.paragraphs[0]
|
||||
title_para.font.size = Pt(32)
|
||||
title_para.font.bold = True
|
||||
title_para.font.color.rgb = title_color
|
||||
|
||||
return slide
|
||||
|
||||
def add_section_slide(prs, section_text, color):
|
||||
"""Add a section divider slide"""
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
|
||||
# Section text
|
||||
section_box = slide.shapes.add_textbox(Inches(1), Inches(2.3), Inches(8), Inches(1))
|
||||
tf = section_box.text_frame
|
||||
tf.text = section_text
|
||||
tf.paragraphs[0].font.size = Pt(40)
|
||||
tf.paragraphs[0].font.bold = True
|
||||
tf.paragraphs[0].font.color.rgb = color
|
||||
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
||||
|
||||
return slide
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Generating PowerPoint presentation...")
|
||||
print("This requires the python-pptx library.")
|
||||
print("If not installed, run: pip install python-pptx\n")
|
||||
|
||||
try:
|
||||
output_path = create_presentation()
|
||||
print(f"\n🎉 Success! Open the presentation at:\n{output_path}")
|
||||
print("\nNext steps:")
|
||||
print("1. Customize contact information (replace [Your Name], [Email], etc.)")
|
||||
print("2. Add your branding/logo if desired")
|
||||
print("3. Adjust colors/fonts to match your style")
|
||||
print("4. Practice delivery (15-20 minutes)")
|
||||
except ImportError:
|
||||
print("❌ Error: python-pptx library not found.")
|
||||
print("Install it with: pip install python-pptx")
|
||||
print("Then run this script again.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating presentation: {e}")
|
||||
|
|
@ -1,443 +0,0 @@
|
|||
/**
|
||||
* Research PDF Generation Script
|
||||
* Generates PDFs from standalone markdown research documents
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const RESEARCH_DIR = path.join(__dirname, '../docs/research');
|
||||
const OUTPUT_DIR = path.join(__dirname, '../public/downloads');
|
||||
|
||||
/**
|
||||
* HTML template for research PDFs
|
||||
*/
|
||||
function generatePdfHtml(title, content, metadata = {}) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Cover page */
|
||||
.cover {
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 80vh;
|
||||
text-align: center;
|
||||
border-bottom: 3px solid #2563eb;
|
||||
padding-bottom: 2cm;
|
||||
}
|
||||
|
||||
.cover h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.cover .metadata {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.cover .metadata p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.875rem;
|
||||
line-height: 1.3;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 0.375rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.4;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.75;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.75rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
ul ul,
|
||||
ol ul {
|
||||
list-style-type: circle;
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
ul ol,
|
||||
ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
color: #1f2937;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f9fafb;
|
||||
color: #1f2937;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #d1d5db;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
color: #6b7280;
|
||||
margin: 1.25rem 0;
|
||||
background-color: #f9fafb;
|
||||
padding: 0.875rem 0.875rem 0.875rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.625rem 0.875rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.625rem 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #d1d5db;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Avoid breaking these elements */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre, blockquote, table, figure {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cover">
|
||||
<h1>${title}</h1>
|
||||
<div class="metadata">
|
||||
${metadata.version ? `<p><strong>Version:</strong> ${metadata.version}</p>` : ''}
|
||||
${metadata.date ? `<p><strong>Date:</strong> ${metadata.date}</p>` : ''}
|
||||
${metadata.type ? `<p><strong>Document Type:</strong> ${metadata.type}</p>` : ''}
|
||||
<p style="margin-top: 2rem; font-style: italic;">Tractatus AI Safety Framework</p>
|
||||
<p style="font-size: 0.875rem;">https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>© ${new Date().getFullYear()} Tractatus AI Safety Framework</p>
|
||||
<p>This document is part of the Tractatus Agentic Governance System</p>
|
||||
<p>https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from markdown frontmatter or header
|
||||
*/
|
||||
function extractMetadata(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const metadata = {};
|
||||
|
||||
// Look for title in first H1
|
||||
const h1Match = markdown.match(/^#\s+(.+)$/m);
|
||||
if (h1Match) {
|
||||
metadata.title = h1Match[1];
|
||||
}
|
||||
|
||||
// Look for version
|
||||
const versionMatch = markdown.match(/\*\*Version:\*\*\s+(.+)/);
|
||||
if (versionMatch) {
|
||||
metadata.version = versionMatch[1];
|
||||
}
|
||||
|
||||
// Look for date
|
||||
const dateMatch = markdown.match(/\*\*Date:\*\*\s+(.+)/);
|
||||
if (dateMatch) {
|
||||
metadata.date = dateMatch[1];
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF from markdown file
|
||||
*/
|
||||
async function generatePdfFromMarkdown(inputFile, outputFilename, browser) {
|
||||
try {
|
||||
console.log(`\nProcessing: ${path.basename(inputFile)}`);
|
||||
|
||||
// Read markdown
|
||||
const markdown = await fs.readFile(inputFile, 'utf-8');
|
||||
|
||||
// Extract metadata
|
||||
const metadata = extractMetadata(markdown);
|
||||
const title = metadata.title || path.basename(inputFile, '.md');
|
||||
|
||||
console.log(` Title: ${title}`);
|
||||
|
||||
// Convert markdown to HTML
|
||||
const contentHtml = marked.parse(markdown);
|
||||
|
||||
// Generate full HTML
|
||||
const html = generatePdfHtml(title, contentHtml, {
|
||||
version: metadata.version,
|
||||
date: metadata.date,
|
||||
type: 'Research Paper'
|
||||
});
|
||||
|
||||
// Create page
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html, { waitUntil: 'networkidle0' });
|
||||
|
||||
// Generate PDF
|
||||
const outputPath = path.join(OUTPUT_DIR, outputFilename);
|
||||
await page.pdf({
|
||||
path: outputPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '2cm',
|
||||
right: '2cm',
|
||||
bottom: '2cm',
|
||||
left: '2cm'
|
||||
},
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; font-size: 9pt; text-align: center; color: #6b7280; padding: 0 2cm;">
|
||||
<span class="pageNumber"></span> / <span class="totalPages"></span>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
await page.close();
|
||||
|
||||
console.log(` ✓ Generated: ${outputFilename}`);
|
||||
return { success: true, filename: outputFilename };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ✗ Failed: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
console.log('=== Research PDF Generation ===');
|
||||
|
||||
let browser;
|
||||
|
||||
try {
|
||||
// Ensure output directory exists
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
console.log(`Output directory: ${OUTPUT_DIR}`);
|
||||
|
||||
// Launch browser
|
||||
console.log('\nLaunching browser...');
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
console.log('✓ Browser ready');
|
||||
|
||||
// Generate PDFs
|
||||
const results = [];
|
||||
|
||||
// Full research paper
|
||||
results.push(await generatePdfFromMarkdown(
|
||||
path.join(RESEARCH_DIR, 'tractatus-inflection-point-2025.md'),
|
||||
'structural-governance-for-agentic-ai-tractatus-inflection-point.pdf',
|
||||
browser
|
||||
));
|
||||
|
||||
// Executive summary
|
||||
results.push(await generatePdfFromMarkdown(
|
||||
path.join(RESEARCH_DIR, 'executive-summary-tractatus-inflection-point.md'),
|
||||
'executive-summary-tractatus-inflection-point.pdf',
|
||||
browser
|
||||
));
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Generation Complete ===\n');
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
console.log(`✓ Successful: ${successful}`);
|
||||
if (failed > 0) {
|
||||
console.log(`✗ Failed: ${failed}`);
|
||||
}
|
||||
console.log(`\nPDFs saved to: ${OUTPUT_DIR}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { generatePdfFromMarkdown, generatePdfHtml };
|
||||
|
|
@ -1,380 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate PDF from Single Markdown File
|
||||
*
|
||||
* Usage: node scripts/generate-single-pdf.js <input.md> [output.pdf]
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const puppeteer = require('puppeteer');
|
||||
const marked = require('marked');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.error('Usage: node scripts/generate-single-pdf.js <input.md> [output.pdf]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const inputPath = path.resolve(args[0]);
|
||||
const outputPath = args[1]
|
||||
? path.resolve(args[1])
|
||||
: inputPath.replace(/\.md$/, '.pdf');
|
||||
|
||||
/**
|
||||
* HTML template for PDF generation
|
||||
*/
|
||||
function generatePdfHtml(title, content) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: A4;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Cover page */
|
||||
.cover {
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 80vh;
|
||||
text-align: center;
|
||||
border-bottom: 3px solid #2563eb;
|
||||
padding-bottom: 2cm;
|
||||
}
|
||||
|
||||
.cover h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.cover .metadata {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.cover .metadata p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
padding-bottom: 0.5rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.875rem;
|
||||
line-height: 1.3;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 0.375rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.4;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.5;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.75;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1.75rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
ul ul,
|
||||
ol ul {
|
||||
list-style-type: circle;
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
ul ol,
|
||||
ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
color: #1f2937;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f9fafb;
|
||||
color: #1f2937;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #d1d5db;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding-left: 1rem;
|
||||
font-style: italic;
|
||||
color: #6b7280;
|
||||
margin: 1.25rem 0;
|
||||
background-color: #f9fafb;
|
||||
padding: 0.875rem 0.875rem 0.875rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.625rem 0.875rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.625rem 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #d1d5db;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Avoid breaking these elements */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
pre, blockquote, table, figure {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cover">
|
||||
<h1>${title}</h1>
|
||||
<div class="metadata">
|
||||
<p><strong>Document Type:</strong> Technical Documentation</p>
|
||||
<p><strong>Generated:</strong> ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||||
<p style="margin-top: 2rem; font-style: italic;">Tractatus AI Safety Framework</p>
|
||||
<p style="font-size: 0.875rem;">https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
${content}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>© ${new Date().getFullYear()} Tractatus AI Safety Framework</p>
|
||||
<p>This document is part of the Tractatus Agentic Governance System</p>
|
||||
<p>https://agenticgovernance.digital</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
console.log('=== PDF Generation from Markdown ===\n');
|
||||
|
||||
let browser;
|
||||
|
||||
try {
|
||||
// Read markdown file
|
||||
console.log(`Reading: ${inputPath}`);
|
||||
const markdown = await fs.readFile(inputPath, 'utf8');
|
||||
|
||||
// Extract title from first # heading
|
||||
const titleMatch = markdown.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : path.basename(inputPath, '.md');
|
||||
|
||||
console.log(`Title: ${title}`);
|
||||
|
||||
// Convert markdown to HTML
|
||||
console.log('Converting markdown to HTML...');
|
||||
const contentHtml = marked.parse(markdown);
|
||||
|
||||
// Generate full HTML
|
||||
const html = generatePdfHtml(title, contentHtml);
|
||||
|
||||
// Launch Puppeteer
|
||||
console.log('Launching browser...');
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Set content
|
||||
console.log('Rendering HTML...');
|
||||
await page.setContent(html, {
|
||||
waitUntil: 'networkidle0'
|
||||
});
|
||||
|
||||
// Generate PDF
|
||||
console.log(`Generating PDF: ${outputPath}`);
|
||||
await page.pdf({
|
||||
path: outputPath,
|
||||
format: 'A4',
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: '2cm',
|
||||
right: '2cm',
|
||||
bottom: '2cm',
|
||||
left: '2cm'
|
||||
},
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: `
|
||||
<div style="width: 100%; font-size: 9pt; text-align: center; color: #6b7280; padding: 0 2cm;">
|
||||
<span class="pageNumber"></span> / <span class="totalPages"></span>
|
||||
</div>
|
||||
`
|
||||
});
|
||||
|
||||
console.log('\n✓ PDF generated successfully!\n');
|
||||
console.log(`Output: ${outputPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run
|
||||
main();
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate Test JWT Token
|
||||
* Creates a valid JWT token for testing Rule Manager API
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
const JWT_EXPIRY = process.env.JWT_EXPIRY || '7d';
|
||||
|
||||
// Admin user from database
|
||||
const payload = {
|
||||
userId: '68e3a6fb21af2fd194bf4b50',
|
||||
email: 'admin@tractatus.local',
|
||||
role: 'admin'
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, JWT_SECRET, {
|
||||
expiresIn: JWT_EXPIRY,
|
||||
audience: 'tractatus-admin',
|
||||
issuer: 'tractatus'
|
||||
});
|
||||
|
||||
console.log('\n=== Test JWT Token ===\n');
|
||||
console.log('Token:', token);
|
||||
console.log('\nUse in Authorization header:');
|
||||
console.log(`Authorization: Bearer ${token}`);
|
||||
console.log('\nExpires in:', JWT_EXPIRY);
|
||||
console.log('');
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Import 5 Missing Archives Documents
|
||||
*
|
||||
* Imports the 5 MD files that are in Archives but not in the database:
|
||||
* 1. Case Studies - Real-World LLM Failure Modes
|
||||
* 2. Python API Integration Examples
|
||||
* 3. Tractatus Framework Enforcement for Claude Code
|
||||
* 4. Concurrent Session Architecture Limitations
|
||||
* 5. Rule Proliferation and Transactional Overhead
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { markdownToHtml, extractTOC, generateSlug } = require('../src/utils/markdown.util');
|
||||
|
||||
const FILES_TO_IMPORT = [
|
||||
{
|
||||
path: '/home/theflow/projects/tractatus/docs/markdown/case-studies.md',
|
||||
slug: 'case-studies-real-world-llm-failure-modes-appendix',
|
||||
title: 'Appendix B: Case Studies - Real-World LLM Failure Modes',
|
||||
category: 'archives',
|
||||
order: 2,
|
||||
archiveNote: 'Internal project tracking document. Not relevant for public documentation.'
|
||||
},
|
||||
{
|
||||
path: '/home/theflow/projects/tractatus/docs/api/examples-python.md',
|
||||
slug: 'implementation-guide-python-examples',
|
||||
title: 'Implementation Guide: Python Code Examples',
|
||||
category: 'archives',
|
||||
order: 3,
|
||||
archiveNote: 'Internal project tracking document. Not relevant for public documentation.'
|
||||
},
|
||||
{
|
||||
path: '/home/theflow/projects/tractatus/docs/claude-code-framework-enforcement.md',
|
||||
slug: 'tractatus-framework-enforcement-claude-code',
|
||||
title: 'Tractatus Framework Enforcement for Claude Code',
|
||||
category: 'archives',
|
||||
order: 4,
|
||||
archiveNote: 'Development tool documentation. See Implementation Guide for production deployment.'
|
||||
},
|
||||
{
|
||||
path: '/home/theflow/projects/tractatus/docs/research/concurrent-session-architecture-limitations.md',
|
||||
slug: 'research-topic-concurrent-session-architecture',
|
||||
title: 'Research Topic: Concurrent Session Architecture Limitations in Claude Code Governance',
|
||||
category: 'archives',
|
||||
order: 5,
|
||||
archiveNote: 'Research analysis. See Architectural Overview for current framework status.'
|
||||
},
|
||||
{
|
||||
path: '/home/theflow/projects/tractatus/docs/research/rule-proliferation-and-transactional-overhead.md',
|
||||
slug: 'research-topic-rule-proliferation-transactional-overhead',
|
||||
title: 'Research Topic: Rule Proliferation and Transactional Overhead in AI Governance',
|
||||
category: 'archives',
|
||||
order: 6,
|
||||
archiveNote: 'Research analysis. See Architectural Overview for current framework status.'
|
||||
}
|
||||
];
|
||||
|
||||
function extractFrontMatter(content) {
|
||||
const frontMatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
||||
const match = content.match(frontMatterRegex);
|
||||
|
||||
if (!match) {
|
||||
return { frontMatter: {}, content };
|
||||
}
|
||||
|
||||
const frontMatterText = match[1];
|
||||
const remainingContent = match[2];
|
||||
|
||||
const frontMatter = {};
|
||||
frontMatterText.split('\n').forEach(line => {
|
||||
const [key, ...valueParts] = line.split(':');
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join(':').trim();
|
||||
frontMatter[key.trim()] = value.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
});
|
||||
|
||||
return { frontMatter, content: remainingContent };
|
||||
}
|
||||
|
||||
async function importDocument(fileInfo) {
|
||||
console.log(`\n📄 Importing: ${fileInfo.title}`);
|
||||
console.log(` File: ${fileInfo.path}`);
|
||||
|
||||
try {
|
||||
// Read markdown file
|
||||
const rawContent = await fs.readFile(fileInfo.path, 'utf-8');
|
||||
const { frontMatter, content } = extractFrontMatter(rawContent);
|
||||
|
||||
// Convert to HTML
|
||||
const content_html = markdownToHtml(content);
|
||||
|
||||
// Extract TOC
|
||||
const toc = extractTOC(content);
|
||||
|
||||
// Check if document already exists
|
||||
const existing = await Document.findBySlug(fileInfo.slug);
|
||||
if (existing) {
|
||||
console.log(` ⚠️ Document already exists: ${fileInfo.slug}`);
|
||||
console.log(` Skipping import.`);
|
||||
return { success: false, reason: 'exists' };
|
||||
}
|
||||
|
||||
// Create document
|
||||
const doc = await Document.create({
|
||||
title: fileInfo.title,
|
||||
slug: fileInfo.slug,
|
||||
quadrant: frontMatter.quadrant || null,
|
||||
persistence: frontMatter.persistence || 'MEDIUM',
|
||||
audience: frontMatter.audience || 'general',
|
||||
visibility: 'archived',
|
||||
category: fileInfo.category,
|
||||
order: fileInfo.order,
|
||||
archiveNote: fileInfo.archiveNote,
|
||||
content_html,
|
||||
content_markdown: content,
|
||||
toc,
|
||||
metadata: {
|
||||
author: frontMatter.author || 'John Stroh',
|
||||
version: frontMatter.version || '1.0',
|
||||
document_code: frontMatter.identifier || null,
|
||||
tags: [],
|
||||
original_filename: path.basename(fileInfo.path),
|
||||
migrated_at: new Date()
|
||||
},
|
||||
search_index: content.toLowerCase(),
|
||||
translations: {},
|
||||
download_formats: {}
|
||||
});
|
||||
|
||||
console.log(` ✅ Imported successfully`);
|
||||
console.log(` ID: ${doc._id}`);
|
||||
console.log(` Slug: ${doc.slug}`);
|
||||
|
||||
return { success: true, doc };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error importing: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🚀 Importing 5 Missing Archives Documents\n');
|
||||
console.log('═══════════════════════════════════════════════════\n');
|
||||
|
||||
await connect();
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const fileInfo of FILES_TO_IMPORT) {
|
||||
const result = await importDocument(fileInfo);
|
||||
if (result.success) {
|
||||
imported++;
|
||||
} else if (result.reason === 'exists') {
|
||||
skipped++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════');
|
||||
console.log('\n📊 Import Summary:');
|
||||
console.log(` ✅ Imported: ${imported}`);
|
||||
console.log(` ⏭️ Skipped (already exists): ${skipped}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📦 Total: ${FILES_TO_IMPORT.length}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Import Coding Best Practice Rules
|
||||
*
|
||||
* Creates governance rules based on Phase 2 error analysis.
|
||||
* These rules prevent common coding errors like schema-code mismatches.
|
||||
*
|
||||
* Usage: node scripts/import-coding-rules.js
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const GovernanceRule = require('../src/models/GovernanceRule.model');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Database connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
|
||||
async function importRules() {
|
||||
try {
|
||||
console.log('🔌 Connecting to MongoDB...');
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log('✅ Connected to database:', MONGODB_URI);
|
||||
|
||||
// Load rules from JSON file
|
||||
const rulesFile = process.argv[2] || path.join(__dirname, '..', 'coding-best-practice-rules.json');
|
||||
const rulesData = JSON.parse(fs.readFileSync(rulesFile, 'utf-8'));
|
||||
|
||||
console.log(`\n📋 Loaded ${rulesData.rules.length} rules from ${rulesFile}\n`);
|
||||
|
||||
// Get current max inst_XXX number
|
||||
const existingRules = await GovernanceRule.find({ id: /^inst_\d+$/ })
|
||||
.sort({ id: -1 })
|
||||
.limit(1);
|
||||
|
||||
let nextNumber = 1;
|
||||
if (existingRules.length > 0) {
|
||||
const lastId = existingRules[0].id; // e.g., "inst_020"
|
||||
nextNumber = parseInt(lastId.split('_')[1]) + 1;
|
||||
}
|
||||
|
||||
console.log(`📊 Next available rule ID: inst_${String(nextNumber).padStart(3, '0')}\n`);
|
||||
|
||||
const results = {
|
||||
created: [],
|
||||
skipped: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
for (const ruleData of rulesData.rules) {
|
||||
const ruleId = `inst_${String(nextNumber).padStart(3, '0')}`;
|
||||
|
||||
try {
|
||||
// Check if similar rule already exists (by text)
|
||||
const existing = await GovernanceRule.findOne({
|
||||
text: ruleData.text,
|
||||
active: true
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
console.log(`⏭️ SKIPPED: ${ruleId} - Similar rule exists (${existing.id})`);
|
||||
console.log(` Text: ${ruleData.text.substring(0, 60)}...\n`);
|
||||
results.skipped.push({ ruleId, reason: `Similar to ${existing.id}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new rule
|
||||
const newRule = new GovernanceRule({
|
||||
id: ruleId,
|
||||
text: ruleData.text,
|
||||
quadrant: ruleData.quadrant,
|
||||
persistence: ruleData.persistence,
|
||||
category: ruleData.category,
|
||||
priority: ruleData.priority,
|
||||
scope: ruleData.scope,
|
||||
applicableProjects: ruleData.applicableProjects || ['*'],
|
||||
temporalScope: ruleData.temporalScope,
|
||||
examples: ruleData.examples || [],
|
||||
notes: ruleData.notes || '',
|
||||
relatedRules: ruleData.relatedRules || [],
|
||||
source: 'automated',
|
||||
createdBy: 'coding_best_practices_import',
|
||||
active: true,
|
||||
|
||||
// AI optimization scores (placeholder - will be calculated by RuleOptimizer)
|
||||
clarityScore: null,
|
||||
specificityScore: null,
|
||||
actionabilityScore: null
|
||||
});
|
||||
|
||||
await newRule.save();
|
||||
|
||||
console.log(`✅ CREATED: ${ruleId}`);
|
||||
console.log(` Quadrant: ${ruleData.quadrant} | Persistence: ${ruleData.persistence}`);
|
||||
console.log(` Text: ${ruleData.text.substring(0, 80)}...`);
|
||||
console.log(` Priority: ${ruleData.priority} | Scope: ${ruleData.scope}\n`);
|
||||
|
||||
results.created.push({
|
||||
id: ruleId,
|
||||
text: ruleData.text.substring(0, 60)
|
||||
});
|
||||
|
||||
nextNumber++;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ FAILED: ${ruleId}`);
|
||||
console.error(` Error: ${error.message}\n`);
|
||||
results.failed.push({
|
||||
ruleId,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('📊 IMPORT SUMMARY');
|
||||
console.log('='.repeat(70));
|
||||
console.log(`✅ Created: ${results.created.length} rules`);
|
||||
console.log(`⏭️ Skipped: ${results.skipped.length} rules`);
|
||||
console.log(`❌ Failed: ${results.failed.length} rules`);
|
||||
console.log(`📋 Total: ${rulesData.rules.length} rules processed\n`);
|
||||
|
||||
if (results.created.length > 0) {
|
||||
console.log('Created Rules:');
|
||||
results.created.forEach(r => {
|
||||
console.log(` - ${r.id}: ${r.text}...`);
|
||||
});
|
||||
}
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log('\n❌ Failed Rules:');
|
||||
results.failed.forEach(r => {
|
||||
console.log(` - ${r.ruleId}: ${r.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n✅ Import complete!');
|
||||
console.log(`📍 View rules: http://localhost:9000/admin/rule-manager.html\n`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Import failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await mongoose.connection.close();
|
||||
console.log('🔌 Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Run import
|
||||
importRules();
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Import Technical Documentation Script
|
||||
* Imports technical documentation (for developers/implementers) into the database
|
||||
*/
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { getDb, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { markdownToHtml, extractTOC } = require('../src/utils/markdown.util');
|
||||
const { validateDocumentSecurity } = require('./validate-document-security');
|
||||
|
||||
// Technical documents to import (audience: technical)
|
||||
// NOTE: Only documents with visibility: 'public' will be imported by default
|
||||
// Documents marked 'internal' or 'confidential' require --allow-internal flag
|
||||
const TECHNICAL_DOCS = [
|
||||
{
|
||||
file: 'docs/claude-code-framework-enforcement.md',
|
||||
title: 'Tractatus Framework Enforcement for Claude Code',
|
||||
slug: 'tractatus-framework-enforcement-for-claude-code',
|
||||
quadrant: 'SYSTEM',
|
||||
persistence: 'HIGH',
|
||||
audience: 'technical',
|
||||
visibility: 'public', // Safe to publish - implementation guide
|
||||
metadata: {
|
||||
author: 'John Stroh',
|
||||
version: '1.0',
|
||||
tags: ['claude-code', 'framework', 'implementation', 'governance'],
|
||||
document_code: 'TECH-001'
|
||||
}
|
||||
}
|
||||
// REMOVED: Security Audit, Koha Stripe Setup, Koha Deployment
|
||||
// These documents contain sensitive information and should NOT be public
|
||||
];
|
||||
|
||||
async function importTechnicalDocs() {
|
||||
console.log('╔══════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Technical Documentation Import ║');
|
||||
console.log('╚══════════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
try {
|
||||
// Connect to database
|
||||
const db = await getDb();
|
||||
console.log('✓ Connected to database\n');
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const docConfig of TECHNICAL_DOCS) {
|
||||
const filePath = path.join(__dirname, '..', docConfig.file);
|
||||
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(`⚠ File not found: ${docConfig.file}`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if document already exists
|
||||
const existing = await Document.findBySlug(docConfig.slug);
|
||||
if (existing) {
|
||||
console.log(`⚠ Already exists: ${docConfig.title}`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read markdown file
|
||||
const content_markdown = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Security validation
|
||||
console.log(` 🔒 Running security validation...`);
|
||||
const securityCheck = validateDocumentSecurity(docConfig, content_markdown);
|
||||
|
||||
if (!securityCheck.valid) {
|
||||
console.log(` ❌ SECURITY VALIDATION FAILED:`);
|
||||
securityCheck.issues.forEach(issue => console.log(` ${issue}`));
|
||||
console.log(` ⚠️ Document blocked from import\n`);
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (securityCheck.warnings.length > 0) {
|
||||
securityCheck.warnings.forEach(warning => console.log(` ${warning}`));
|
||||
}
|
||||
|
||||
// Convert to HTML
|
||||
const content_html = markdownToHtml(content_markdown);
|
||||
|
||||
// Extract TOC
|
||||
const toc = extractTOC(content_markdown);
|
||||
|
||||
// Create search index
|
||||
const search_index = `${docConfig.title} ${content_markdown}`.toLowerCase();
|
||||
|
||||
// Create document
|
||||
const document = await Document.create({
|
||||
title: docConfig.title,
|
||||
slug: docConfig.slug,
|
||||
quadrant: docConfig.quadrant,
|
||||
persistence: docConfig.persistence,
|
||||
audience: docConfig.audience,
|
||||
visibility: docConfig.visibility || 'public',
|
||||
security_classification: securityCheck.classification,
|
||||
content_html,
|
||||
content_markdown,
|
||||
toc,
|
||||
metadata: {
|
||||
...docConfig.metadata,
|
||||
date_created: new Date(),
|
||||
date_updated: new Date()
|
||||
},
|
||||
search_index,
|
||||
public: docConfig.visibility === 'public'
|
||||
});
|
||||
|
||||
console.log(`✓ Imported: ${docConfig.title}`);
|
||||
console.log(` Slug: ${docConfig.slug}`);
|
||||
console.log(` Audience: ${docConfig.audience}`);
|
||||
console.log(` Quadrant: ${docConfig.quadrant}\n`);
|
||||
imported++;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`✗ Error importing ${docConfig.file}:`, error.message);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('─────────────────────────────────────────────────────────────────');
|
||||
console.log(`Summary:`);
|
||||
console.log(` Imported: ${imported}`);
|
||||
console.log(` Skipped: ${skipped}`);
|
||||
console.log(` Errors: ${errors}`);
|
||||
console.log('─────────────────────────────────────────────────────────────────\n');
|
||||
|
||||
if (errors === 0) {
|
||||
console.log('✓ Technical documentation import completed successfully\n');
|
||||
} else {
|
||||
console.log('⚠ Import completed with errors\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Import failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run import
|
||||
if (require.main === module) {
|
||||
importTechnicalDocs();
|
||||
}
|
||||
|
||||
module.exports = { importTechnicalDocs };
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Initialize Koha Donation System
|
||||
* Creates database indexes and verifies setup
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { connect, close, getCollection } = require('../src/utils/db.util');
|
||||
const Donation = require('../src/models/Donation.model');
|
||||
|
||||
async function initializeKoha() {
|
||||
console.log('\n🎁 Koha Donation System Initialization\n');
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
console.log('📦 Connecting to MongoDB...');
|
||||
await connect();
|
||||
console.log('✅ Connected to database:', process.env.MONGODB_DB);
|
||||
|
||||
// Create indexes
|
||||
console.log('\n📊 Creating database indexes...');
|
||||
await Donation.createIndexes();
|
||||
console.log('✅ Indexes created successfully');
|
||||
|
||||
// Verify collection exists
|
||||
const collection = await getCollection('koha_donations');
|
||||
const indexes = await collection.indexes();
|
||||
console.log(`\n✅ Collection 'koha_donations' ready with ${indexes.length} indexes:`);
|
||||
indexes.forEach((index, i) => {
|
||||
const keys = Object.keys(index.key).join(', ');
|
||||
console.log(` ${i + 1}. ${index.name} (${keys})`);
|
||||
});
|
||||
|
||||
// Check for existing donations
|
||||
const count = await collection.countDocuments();
|
||||
console.log(`\n📈 Current donations in database: ${count}`);
|
||||
|
||||
// Test transparency metrics calculation
|
||||
console.log('\n🔍 Testing transparency metrics...');
|
||||
const metrics = await Donation.getTransparencyMetrics();
|
||||
console.log('✅ Transparency metrics calculated:');
|
||||
console.log(` - Total received: $${metrics.total_received.toFixed(2)} NZD`);
|
||||
console.log(` - Monthly supporters: ${metrics.monthly_supporters}`);
|
||||
console.log(` - One-time donations: ${metrics.one_time_donations}`);
|
||||
console.log(` - Monthly recurring revenue: $${metrics.monthly_recurring_revenue.toFixed(2)} NZD`);
|
||||
console.log(` - Public donors: ${metrics.recent_donors.length}`);
|
||||
|
||||
// Verify Stripe configuration
|
||||
console.log('\n🔑 Verifying Stripe configuration...');
|
||||
const stripeConfig = {
|
||||
secretKey: process.env.STRIPE_SECRET_KEY ? '✅ Set' : '❌ Missing',
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY ? '✅ Set' : '❌ Missing',
|
||||
webhookSecret: process.env.STRIPE_KOHA_WEBHOOK_SECRET ? '✅ Set' : '❌ Missing',
|
||||
price5: process.env.STRIPE_KOHA_5_PRICE_ID ? '✅ Set' : '⚠️ Missing (needs Stripe Dashboard setup)',
|
||||
price15: process.env.STRIPE_KOHA_15_PRICE_ID ? '✅ Set' : '⚠️ Missing (needs Stripe Dashboard setup)',
|
||||
price50: process.env.STRIPE_KOHA_50_PRICE_ID ? '✅ Set' : '⚠️ Missing (needs Stripe Dashboard setup)'
|
||||
};
|
||||
|
||||
console.log(` - Secret Key: ${stripeConfig.secretKey}`);
|
||||
console.log(` - Publishable Key: ${stripeConfig.publishableKey}`);
|
||||
console.log(` - Webhook Secret: ${stripeConfig.webhookSecret}`);
|
||||
console.log(` - $5 NZD Price ID: ${stripeConfig.price5}`);
|
||||
console.log(` - $15 NZD Price ID: ${stripeConfig.price15}`);
|
||||
console.log(` - $50 NZD Price ID: ${stripeConfig.price50}`);
|
||||
|
||||
// Warning if price IDs not set
|
||||
if (stripeConfig.price5.includes('Missing') ||
|
||||
stripeConfig.price15.includes('Missing') ||
|
||||
stripeConfig.price50.includes('Missing')) {
|
||||
console.log('\n⚠️ WARNING: Stripe Price IDs not configured!');
|
||||
console.log(' Follow the guide: docs/KOHA_STRIPE_SETUP.md');
|
||||
console.log(' Create products in Stripe Dashboard and update .env');
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n✅ Koha system initialized successfully!');
|
||||
console.log('\n📋 Next steps:');
|
||||
console.log(' 1. Create Stripe products (if not done): docs/KOHA_STRIPE_SETUP.md');
|
||||
console.log(' 2. Start server: npm run dev');
|
||||
console.log(' 3. Test donation form: http://localhost:9000/koha.html');
|
||||
console.log(' 4. View transparency dashboard: http://localhost:9000/koha/transparency.html');
|
||||
console.log(' 5. Test API endpoint: POST /api/koha/checkout\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Initialization failed:', error.message);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
initializeKoha();
|
||||
}
|
||||
|
||||
module.exports = initializeKoha;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Install mongodb-tractatus systemd service
|
||||
# Run with: sudo ./install-mongodb-service.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SERVICE_NAME="mongodb-tractatus.service"
|
||||
SERVICE_FILE="./mongodb-tractatus.service"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
|
||||
echo "Installing MongoDB Tractatus systemd service..."
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Error: This script must be run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if service file exists
|
||||
if [ ! -f "$SERVICE_FILE" ]; then
|
||||
echo "Error: $SERVICE_FILE not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy service file to systemd directory
|
||||
echo "Copying service file to $SYSTEMD_DIR..."
|
||||
cp "$SERVICE_FILE" "$SYSTEMD_DIR/$SERVICE_NAME"
|
||||
|
||||
# Set correct permissions
|
||||
chmod 644 "$SYSTEMD_DIR/$SERVICE_NAME"
|
||||
|
||||
# Reload systemd daemon
|
||||
echo "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable service to start on boot
|
||||
echo "Enabling service to start on boot..."
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
|
||||
echo ""
|
||||
echo "MongoDB Tractatus service installed successfully!"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " Start: sudo systemctl start $SERVICE_NAME"
|
||||
echo " Stop: sudo systemctl stop $SERVICE_NAME"
|
||||
echo " Restart: sudo systemctl restart $SERVICE_NAME"
|
||||
echo " Status: sudo systemctl status $SERVICE_NAME"
|
||||
echo " Logs: sudo journalctl -u $SERVICE_NAME -f"
|
||||
echo ""
|
||||
echo "To start the service now, run:"
|
||||
echo " sudo systemctl start $SERVICE_NAME"
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Tractatus systemd Service Installation Script
|
||||
# Usage: ./scripts/install-systemd.sh [dev|prod]
|
||||
|
||||
set -e
|
||||
|
||||
ENVIRONMENT=${1:-dev}
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
if [ "$ENVIRONMENT" = "dev" ]; then
|
||||
SERVICE_FILE="tractatus-dev.service"
|
||||
SERVICE_NAME="tractatus-dev"
|
||||
echo "Installing Tractatus Development Service..."
|
||||
elif [ "$ENVIRONMENT" = "prod" ]; then
|
||||
SERVICE_FILE="tractatus-prod.service"
|
||||
SERVICE_NAME="tractatus"
|
||||
echo "Installing Tractatus Production Service..."
|
||||
else
|
||||
echo "Error: Invalid environment. Use 'dev' or 'prod'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if systemd service file exists
|
||||
if [ ! -f "$PROJECT_ROOT/systemd/$SERVICE_FILE" ]; then
|
||||
echo "Error: Service file not found: $PROJECT_ROOT/systemd/$SERVICE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop existing service if running
|
||||
echo "Stopping existing service (if running)..."
|
||||
sudo systemctl stop $SERVICE_NAME 2>/dev/null || true
|
||||
|
||||
# Copy service file to systemd directory
|
||||
echo "Installing service file..."
|
||||
sudo cp "$PROJECT_ROOT/systemd/$SERVICE_FILE" "/etc/systemd/system/$SERVICE_NAME.service"
|
||||
|
||||
# Set proper permissions
|
||||
sudo chmod 644 "/etc/systemd/system/$SERVICE_NAME.service"
|
||||
|
||||
# Reload systemd daemon
|
||||
echo "Reloading systemd daemon..."
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Enable service to start on boot
|
||||
echo "Enabling service to start on boot..."
|
||||
sudo systemctl enable $SERVICE_NAME
|
||||
|
||||
# Start the service
|
||||
echo "Starting service..."
|
||||
sudo systemctl start $SERVICE_NAME
|
||||
|
||||
# Show status
|
||||
echo ""
|
||||
echo "Service installation complete!"
|
||||
echo ""
|
||||
sudo systemctl status $SERVICE_NAME --no-pager
|
||||
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " sudo systemctl status $SERVICE_NAME # Check status"
|
||||
echo " sudo systemctl restart $SERVICE_NAME # Restart service"
|
||||
echo " sudo systemctl stop $SERVICE_NAME # Stop service"
|
||||
echo " sudo journalctl -u $SERVICE_NAME -f # View logs (follow)"
|
||||
echo " sudo journalctl -u $SERVICE_NAME --since today # Today's logs"
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
const { getDb } = require('../src/utils/db.util');
|
||||
|
||||
(async () => {
|
||||
const db = await getDb();
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Get all archived, confidential, and internal documents
|
||||
const docs = await collection.find({
|
||||
visibility: { $in: ['archived', 'confidential', 'internal'] }
|
||||
}).project({ slug: 1, title: 1, visibility: 1 }).sort({ visibility: 1, slug: 1 }).toArray();
|
||||
|
||||
console.log('\n📋 Documents with visibility: archived, confidential, internal\n');
|
||||
|
||||
const byVisibility = {
|
||||
archived: [],
|
||||
confidential: [],
|
||||
internal: []
|
||||
};
|
||||
|
||||
docs.forEach(doc => {
|
||||
byVisibility[doc.visibility] = byVisibility[doc.visibility] || [];
|
||||
byVisibility[doc.visibility].push(doc);
|
||||
});
|
||||
|
||||
Object.entries(byVisibility).forEach(([visibility, documents]) => {
|
||||
if (documents.length > 0) {
|
||||
console.log(`\n${visibility.toUpperCase()} (${documents.length}):`);
|
||||
documents.forEach(doc => {
|
||||
console.log(` - ${doc.slug}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n\nTotal: ${docs.length} documents\n`);
|
||||
process.exit(0);
|
||||
})();
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Load Governance Rules into Database
|
||||
*
|
||||
* Loads governance rules from JSON file into MongoDB
|
||||
*
|
||||
* Usage: node scripts/load-governance-rules.js <rules-file.json>
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { MongoClient } = require('mongodb');
|
||||
require('dotenv').config();
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_prod';
|
||||
|
||||
async function loadGovernanceRules(rulesFile) {
|
||||
console.log('🔧 Loading Governance Rules...\n');
|
||||
|
||||
// Read rules file
|
||||
const rulesPath = path.resolve(process.cwd(), rulesFile);
|
||||
|
||||
if (!fs.existsSync(rulesPath)) {
|
||||
console.error(`❌ Error: Rules file not found: ${rulesPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let rulesData;
|
||||
try {
|
||||
const fileContent = fs.readFileSync(rulesPath, 'utf8');
|
||||
rulesData = JSON.parse(fileContent);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error parsing rules file: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!rulesData.rules || !Array.isArray(rulesData.rules)) {
|
||||
console.error('❌ Error: Invalid rules file format (missing "rules" array)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📄 Found ${rulesData.rules.length} rules in ${path.basename(rulesFile)}`);
|
||||
|
||||
// Connect to MongoDB
|
||||
const client = new MongoClient(MONGODB_URI);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('✓ Connected to MongoDB\n');
|
||||
|
||||
const db = client.db();
|
||||
const rulesCollection = db.collection('governance_rules');
|
||||
|
||||
// Clear existing rules (optional - comment out to append instead)
|
||||
const deleteResult = await rulesCollection.deleteMany({});
|
||||
if (deleteResult.deletedCount > 0) {
|
||||
console.log(`🗑️ Cleared ${deleteResult.deletedCount} existing rules\n`);
|
||||
}
|
||||
|
||||
// Insert rules
|
||||
const rules = rulesData.rules.map(rule => ({
|
||||
...rule,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
active: true,
|
||||
source: 'manual_load',
|
||||
version: rulesData.version || '1.0.0'
|
||||
}));
|
||||
|
||||
const insertResult = await rulesCollection.insertMany(rules);
|
||||
console.log(`✓ Inserted ${insertResult.insertedCount} governance rules\n`);
|
||||
|
||||
// Create indexes
|
||||
await rulesCollection.createIndex({ rule_id: 1 }, { unique: true });
|
||||
await rulesCollection.createIndex({ quadrant: 1 });
|
||||
await rulesCollection.createIndex({ persistence: 1 });
|
||||
await rulesCollection.createIndex({ enforced_by: 1 });
|
||||
console.log('✓ Created indexes\n');
|
||||
|
||||
// Summary
|
||||
console.log('╔════════════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Rules Loaded Successfully ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
// Count by quadrant
|
||||
const quadrantCounts = await rulesCollection.aggregate([
|
||||
{ $group: { _id: '$quadrant', count: { $sum: 1 } } },
|
||||
{ $sort: { _id: 1 } }
|
||||
]).toArray();
|
||||
|
||||
console.log('Rules by Quadrant:');
|
||||
quadrantCounts.forEach(({ _id, count }) => {
|
||||
console.log(` ${_id}: ${count}`);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
|
||||
// Count by service
|
||||
const serviceCounts = await rulesCollection.aggregate([
|
||||
{ $group: { _id: '$enforced_by', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]).toArray();
|
||||
|
||||
console.log('Rules by Service:');
|
||||
serviceCounts.forEach(({ _id, count }) => {
|
||||
console.log(` ${_id}: ${count}`);
|
||||
});
|
||||
|
||||
console.log('\n✅ Governance rules successfully loaded!\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading rules:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Usage: node scripts/load-governance-rules.js <rules-file.json>');
|
||||
console.error('Example: node scripts/load-governance-rules.js deployment-quickstart/sample-governance-rules.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rulesFile = process.argv[2];
|
||||
loadGovernanceRules(rulesFile);
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Load inst_035 (Precedent Database Design) into MongoDB
|
||||
* This resolves the startup warning about missing inst_035
|
||||
*/
|
||||
|
||||
// Load environment variables from .env file
|
||||
require('dotenv').config();
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const config = require('../src/config/app.config');
|
||||
|
||||
const instructionData = {
|
||||
id: "inst_035",
|
||||
text: "Precedent database stores past deliberations as INFORMATIVE (not binding) precedents. Each entry documents: decision context, moral frameworks in tension, stakeholders consulted, values prioritized/deprioritized, moral remainder, dissenting views, justification, precedent applicability SCOPE (not universal rule), review date. When similar case arises: (1) CrossReferenceValidator identifies relevant precedents, (2) Human reviews for context similarity, (3) Precedent INFORMS new deliberation but doesn't dictate outcome, (4) Document why following or departing from precedent. Precedents are PROVISIONAL - reviewable when context changes, scale shifts, new evidence emerges. Prevent precedent creep into rigid hierarchy.",
|
||||
timestamp: "2025-10-12T14:35:00Z",
|
||||
quadrant: "OPERATIONAL",
|
||||
persistence: "HIGH",
|
||||
temporal_scope: "PERMANENT",
|
||||
verification_required: "MANDATORY",
|
||||
explicitness: 1.0,
|
||||
source: "user",
|
||||
session_id: "2025-10-12-value-pluralism-implementation",
|
||||
parameters: {
|
||||
precedent_type: "informative_not_binding",
|
||||
precedent_fields: [
|
||||
"context",
|
||||
"frameworks_in_tension",
|
||||
"stakeholders",
|
||||
"values_prioritized",
|
||||
"values_deprioritized",
|
||||
"moral_remainder",
|
||||
"dissent",
|
||||
"justification",
|
||||
"applicability_scope",
|
||||
"review_date"
|
||||
],
|
||||
precedent_matching: "CrossReferenceValidator identifies similar cases",
|
||||
human_review_required: "Context similarity assessment",
|
||||
precedent_role: "Informs, doesn't dictate",
|
||||
departure_documentation: "Explain why not following precedent",
|
||||
provisional_nature: "Reviewable when context/scale/evidence changes",
|
||||
prevent: "Precedent creep into universal rules",
|
||||
related_component: [
|
||||
"PluralisticDeliberationOrchestrator",
|
||||
"CrossReferenceValidator"
|
||||
]
|
||||
},
|
||||
active: true,
|
||||
notes: "CORE VALUE PLURALISM IMPLEMENTATION 2025-10-12 - Precedent database design prevents rigid hierarchy while enabling learning from past deliberations. Precedents are PROVISIONAL (Gutmann & Thompson) - decisions aren't final, they're revisable. Key distinction: precedent = 'in similar past case we did X' NOT 'therefore you must do X'. Context matters: scale changes (1000 users → 87 million users = re-deliberate), new evidence (theoretical harm now documented = re-deliberate), changed circumstances = review. Git-like versioning tracks how thinking evolved over time."
|
||||
};
|
||||
|
||||
async function loadInstruction() {
|
||||
try {
|
||||
console.log('🔌 Connecting to MongoDB...');
|
||||
await mongoose.connect(config.mongodb.uri, config.mongodb.options);
|
||||
console.log('✅ Connected to MongoDB');
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
const collection = db.collection('governanceRules'); // Must match model collection name
|
||||
|
||||
// Check if inst_035 already exists
|
||||
const existing = await collection.findOne({ id: 'inst_035' });
|
||||
|
||||
if (existing) {
|
||||
console.log('⚠️ inst_035 already exists in database');
|
||||
console.log(' Updating with latest version...');
|
||||
|
||||
await collection.updateOne(
|
||||
{ id: 'inst_035' },
|
||||
{ $set: instructionData }
|
||||
);
|
||||
|
||||
console.log('✅ inst_035 updated successfully');
|
||||
} else {
|
||||
console.log('📝 Inserting inst_035...');
|
||||
await collection.insertOne(instructionData);
|
||||
console.log('✅ inst_035 inserted successfully');
|
||||
}
|
||||
|
||||
// Verify insertion
|
||||
const inserted = await collection.findOne({ id: 'inst_035' });
|
||||
console.log('\n📋 Verification:');
|
||||
console.log(` ID: ${inserted.id}`);
|
||||
console.log(` Quadrant: ${inserted.quadrant}`);
|
||||
console.log(` Persistence: ${inserted.persistence}`);
|
||||
console.log(` Text: ${inserted.text.substring(0, 80)}...`);
|
||||
console.log(` Related Components: ${inserted.parameters.related_component.join(', ')}`);
|
||||
|
||||
await mongoose.connection.close();
|
||||
console.log('\n✅ Done! MongoDB connection closed.');
|
||||
process.exit(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading instruction:', error);
|
||||
await mongoose.connection.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
loadInstruction();
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migrate Appendix Documents to Technical Documentation
|
||||
*
|
||||
* Updates document records with:
|
||||
* - New descriptive titles (remove "Appendix" prefix)
|
||||
* - Audience classification (technical/researcher)
|
||||
* - Updated PDF download paths
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close, getCollection } = require('../src/utils/db.util');
|
||||
|
||||
const MIGRATIONS = [
|
||||
{
|
||||
// Appendix A: Code Examples → Implementation Guide
|
||||
oldFile: 'appendix-a-code-examples-and-implementation-details.pdf',
|
||||
newFile: 'implementation-guide-python-code-examples.pdf',
|
||||
updates: {
|
||||
title: 'Implementation Guide: Python Code Examples',
|
||||
slug: 'implementation-guide-python-code-examples',
|
||||
audience: 'technical',
|
||||
'metadata.tags': ['implementation', 'code-examples', 'python', 'technical'],
|
||||
'download_formats.pdf': '/downloads/implementation-guide-python-code-examples.pdf'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Appendix B: Case Studies (already properly named, just update audience)
|
||||
oldFile: 'case-studies-real-world-llm-failure-modes.pdf',
|
||||
newFile: 'case-studies-real-world-llm-failure-modes.pdf',
|
||||
updates: {
|
||||
audience: 'technical', // Dual audience: technical + researcher
|
||||
'metadata.tags': ['case-studies', 'failures', 'research', 'technical'],
|
||||
}
|
||||
},
|
||||
{
|
||||
// Appendix C: Implementation Roadmap
|
||||
oldFile: 'appendix-c-implementation-roadmap.pdf',
|
||||
newFile: 'implementation-roadmap-24-month-deployment-plan.pdf',
|
||||
updates: {
|
||||
title: 'Implementation Roadmap: 24-Month Deployment Plan',
|
||||
slug: 'implementation-roadmap-24-month-deployment-plan',
|
||||
audience: 'technical',
|
||||
'metadata.tags': ['roadmap', 'deployment', 'planning', 'technical'],
|
||||
'download_formats.pdf': '/downloads/implementation-roadmap-24-month-deployment-plan.pdf'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Appendix D: Research Review
|
||||
oldFile: 'appendix-d-research-review-and-scholarly-context.pdf',
|
||||
newFile: 'research-foundations-scholarly-review-and-context.pdf',
|
||||
updates: {
|
||||
title: 'Research Foundations: Scholarly Review and Context',
|
||||
slug: 'research-foundations-scholarly-review-and-context',
|
||||
audience: 'researcher',
|
||||
'metadata.tags': ['research', 'scholarly', 'academic', 'foundations'],
|
||||
'download_formats.pdf': '/downloads/research-foundations-scholarly-review-and-context.pdf'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async function migrate() {
|
||||
console.log('\n╔════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Migrate Appendix Documents to Technical Documentation ║');
|
||||
console.log('╚════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
try {
|
||||
await connect();
|
||||
console.log('✓ Connected to MongoDB\n');
|
||||
|
||||
const collection = await getCollection('documents');
|
||||
|
||||
for (const migration of MIGRATIONS) {
|
||||
console.log(`\n▶ Processing: ${migration.oldFile}`);
|
||||
|
||||
// Find document by old filename
|
||||
const query = {
|
||||
$or: [
|
||||
{ 'download_formats.pdf': { $regex: migration.oldFile } },
|
||||
{ slug: migration.oldFile.replace('.pdf', '') }
|
||||
]
|
||||
};
|
||||
|
||||
const doc = await collection.findOne(query);
|
||||
|
||||
if (!doc) {
|
||||
console.log(` ⚠ Document not found in database - may need manual creation`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update document
|
||||
const result = await collection.updateOne(
|
||||
{ _id: doc._id },
|
||||
{
|
||||
$set: {
|
||||
...migration.updates,
|
||||
'metadata.date_updated': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
console.log(` ✓ Updated: ${migration.updates.title || doc.title}`);
|
||||
console.log(` - Audience: ${migration.updates.audience}`);
|
||||
console.log(` - New file: ${migration.newFile}`);
|
||||
} else {
|
||||
console.log(` ⚠ No changes made (already up to date)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n\n═══════════════════════════════════════════════════════');
|
||||
console.log('Migration complete!');
|
||||
console.log('═══════════════════════════════════════════════════════\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Migration failed:', error.message);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
if (require.main === module) {
|
||||
migrate();
|
||||
}
|
||||
|
||||
module.exports = migrate;
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Migrate documents to new granular category structure
|
||||
*
|
||||
* New Categories:
|
||||
* - getting-started: Introduction, core concepts, quick start, FAQ
|
||||
* - technical-reference: API docs, implementation guides, code examples
|
||||
* - research-theory: Research papers, theoretical foundations
|
||||
* - case-studies: Real-world examples, failure modes, success stories
|
||||
* - deployment-operations: Deployment guides, architecture, troubleshooting
|
||||
* - business-leadership: Business cases, executive briefs, ROI analysis
|
||||
* - downloads-resources: PDFs, sample configs, external links
|
||||
*/
|
||||
|
||||
// Load environment variables
|
||||
require('dotenv').config();
|
||||
|
||||
const { getDb } = require('../src/utils/db.util');
|
||||
|
||||
// Slug-to-category mapping (precise, no fuzzy matching)
|
||||
const slugToCategory = {
|
||||
// Getting Started
|
||||
'introduction-to-the-tractatus-framework': 'getting-started',
|
||||
'core-concepts-of-the-tractatus-framework': 'getting-started',
|
||||
|
||||
// Technical Reference
|
||||
'api-reference-complete': 'technical-reference',
|
||||
'openapi-specification': 'technical-reference',
|
||||
'implementation-guide': 'technical-reference',
|
||||
'tractatus-framework-implementation-guide': 'technical-reference',
|
||||
'api-python-examples': 'technical-reference',
|
||||
'api-javascript-examples': 'technical-reference',
|
||||
'technical-architecture-diagram': 'technical-reference',
|
||||
'technical-architecture': 'technical-reference',
|
||||
'tractatus-agentic-governance-system-glossary-of-terms': 'technical-reference',
|
||||
'comparison-matrix-claude-code-claudemd-and-tractatus-framework': 'technical-reference',
|
||||
|
||||
// Research & Theory
|
||||
'organizational-theory-foundations-of-the-tractatus-framework': 'research-theory',
|
||||
'research-foundations-scholarly-review-and-context': 'research-theory',
|
||||
'executive-brief-tractatus-based-llm-architecture-for-ai-safety': 'research-theory',
|
||||
'structural-governance-for-agentic-ai-the-tractatus-inflection-point': 'research-theory',
|
||||
'tractatus-ai-safety-framework-core-values-and-principles': 'research-theory',
|
||||
'architectural-overview-and-research-status': 'research-theory',
|
||||
|
||||
// Advanced Topics
|
||||
'value-pluralism-faq': 'advanced-topics',
|
||||
'pluralistic-values-research-foundations': 'advanced-topics',
|
||||
'pluralistic-values-deliberation-plan-v2': 'advanced-topics',
|
||||
'research-scope-feasibility-of-llm-integrated-tractatus-framework': 'advanced-topics',
|
||||
'research-topic-rule-proliferation-and-transactional-overhead-in-ai-governance': 'advanced-topics',
|
||||
'research-topic-concurrent-session-architecture-limitations-in-claude-code-governance': 'advanced-topics',
|
||||
|
||||
// Case Studies
|
||||
'case-studies-real-world-llm-failure-modes': 'case-studies',
|
||||
'the-27027-incident-a-case-study-in-pattern-recognition-bias': 'case-studies',
|
||||
'our-framework-in-action-detecting-and-correcting-ai-fabrications': 'case-studies',
|
||||
'framework-governance-in-action-pre-publication-security-audit': 'case-studies',
|
||||
'when-frameworks-fail-and-why-thats-ok': 'case-studies',
|
||||
'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery': 'case-studies',
|
||||
|
||||
// Business & Leadership
|
||||
'ai-governance-business-case-template-tractatus-framework': 'business-leadership',
|
||||
'business-case-for-ai-governance-tractatus-framework': 'business-leadership',
|
||||
'executive-summary-tractatus-inflection-point': 'business-leadership',
|
||||
'implementation-roadmap-24-month-deployment-plan': 'business-leadership',
|
||||
|
||||
// Archived
|
||||
'phase-2': 'archived',
|
||||
'phase-3': 'archived',
|
||||
'phase-5-poc-session-1-summary': 'archived',
|
||||
'phase-5-poc-session-2-summary': 'archived'
|
||||
};
|
||||
|
||||
async function migrateCategories() {
|
||||
console.log('🔄 Migrating document categories to new structure...\n');
|
||||
|
||||
const db = await getDb();
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Get all documents
|
||||
const documents = await collection.find({}).toArray();
|
||||
console.log(`📚 Found ${documents.length} documents\n`);
|
||||
|
||||
let updated = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const doc of documents) {
|
||||
try {
|
||||
// Use slug-based mapping (precise)
|
||||
const newCategory = slugToCategory[doc.slug] || 'technical-reference';
|
||||
|
||||
// Update document if category changed
|
||||
if (doc.category !== newCategory) {
|
||||
await collection.updateOne(
|
||||
{ _id: doc._id },
|
||||
{
|
||||
$set: {
|
||||
category: newCategory,
|
||||
'metadata.date_updated': new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`✅ Updated: "${doc.title}"`);
|
||||
console.log(` Category: ${doc.category || 'none'} → ${newCategory}\n`);
|
||||
updated++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error updating "${doc.title}":`, error.message);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 Migration Summary:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Total documents: ${documents.length}`);
|
||||
console.log(`✅ Updated: ${updated}`);
|
||||
console.log(`⏭️ Unchanged: ${documents.length - updated - errors}`);
|
||||
console.log(`❌ Errors: ${errors}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// Show category breakdown
|
||||
console.log('\n📂 Category Breakdown:');
|
||||
const categoryCounts = await collection.aggregate([
|
||||
{ $group: { _id: '$category', count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } }
|
||||
]).toArray();
|
||||
|
||||
categoryCounts.forEach(({ _id, count }) => {
|
||||
console.log(` ${_id || 'none'}: ${count}`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run migration
|
||||
migrateCategories().catch(error => {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,390 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migration: Comprehensive Document Recategorization
|
||||
*
|
||||
* This migration:
|
||||
* 1. Renames "Value Pluralism FAQ" to "Understanding Value Pluralism"
|
||||
* 2. Recategorizes all documents into proper categories
|
||||
* 3. Restores relevant archived documents to public visibility
|
||||
* 4. Assigns proper order values for intuitive sidebar organization
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
const MONGO_URI = process.env.MONGODB_URI || process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||
const DB_NAME = process.env.MONGODB_DB || process.env.MONGO_DB || 'tractatus_dev';
|
||||
|
||||
// Document updates: { slug, updates }
|
||||
const DOCUMENT_UPDATES = [
|
||||
// ========================================
|
||||
// GETTING STARTED
|
||||
// ========================================
|
||||
{
|
||||
slug: 'introduction-to-the-tractatus-framework',
|
||||
updates: {
|
||||
category: 'getting-started',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'architectural-overview-and-research-status',
|
||||
updates: {
|
||||
category: 'getting-started',
|
||||
visibility: 'public',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'core-concepts-of-the-tractatus-framework',
|
||||
updates: {
|
||||
category: 'getting-started',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'tractatus-ai-safety-framework-core-values-and-principles',
|
||||
updates: {
|
||||
category: 'getting-started',
|
||||
visibility: 'public',
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'tractatus-agentic-governance-system-glossary-of-terms',
|
||||
updates: {
|
||||
category: 'getting-started',
|
||||
visibility: 'public',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// TECHNICAL REFERENCE
|
||||
// ========================================
|
||||
{
|
||||
slug: 'technical-architecture',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'implementation-guide',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'tractatus-framework-implementation-guide',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'api-reference-complete',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'api-javascript-examples',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'api-python-examples',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 6
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'openapi-specification',
|
||||
updates: {
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 7
|
||||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// THEORY & RESEARCH
|
||||
// ========================================
|
||||
{
|
||||
slug: 'research-foundations-scholarly-review-and-context',
|
||||
updates: {
|
||||
category: 'research-theory',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'organizational-theory-foundations-of-the-tractatus-framework',
|
||||
updates: {
|
||||
category: 'research-theory',
|
||||
visibility: 'public',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'pluralistic-values-research-foundations',
|
||||
updates: {
|
||||
category: 'research-theory',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'research-topic-rule-proliferation-and-transactional-overhead-in-ai-governance',
|
||||
updates: {
|
||||
category: 'research-theory',
|
||||
visibility: 'public',
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'research-topic-concurrent-session-architecture-limitations-in-claude-code-governance',
|
||||
updates: {
|
||||
category: 'research-theory',
|
||||
visibility: 'public',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// ADVANCED TOPICS
|
||||
// ========================================
|
||||
{
|
||||
slug: 'value-pluralism-faq',
|
||||
updates: {
|
||||
title: 'Understanding Value Pluralism in Tractatus',
|
||||
category: 'advanced-topics',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'pluralistic-values-deliberation-plan-v2',
|
||||
updates: {
|
||||
category: 'advanced-topics',
|
||||
visibility: 'public',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'comparison-matrix-claude-code-claudemd-and-tractatus-framework',
|
||||
updates: {
|
||||
category: 'advanced-topics',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'research-scope-feasibility-of-llm-integrated-tractatus-framework',
|
||||
updates: {
|
||||
category: 'advanced-topics',
|
||||
visibility: 'public',
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// CASE STUDIES
|
||||
// ========================================
|
||||
{
|
||||
slug: 'the-27027-incident-a-case-study-in-pattern-recognition-bias',
|
||||
updates: {
|
||||
category: 'case-studies',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'when-frameworks-fail-and-why-thats-ok',
|
||||
updates: {
|
||||
category: 'case-studies',
|
||||
visibility: 'public',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'our-framework-in-action-detecting-and-correcting-ai-fabrications',
|
||||
updates: {
|
||||
category: 'case-studies',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery',
|
||||
updates: {
|
||||
category: 'case-studies',
|
||||
visibility: 'public',
|
||||
order: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'framework-governance-in-action-pre-publication-security-audit',
|
||||
updates: {
|
||||
category: 'case-studies',
|
||||
visibility: 'public',
|
||||
order: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'case-studies-real-world-llm-failure-modes',
|
||||
updates: {
|
||||
category: 'case-studies',
|
||||
visibility: 'public',
|
||||
order: 6
|
||||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// BUSINESS & LEADERSHIP
|
||||
// ========================================
|
||||
{
|
||||
slug: 'executive-brief-tractatus-based-llm-architecture-for-ai-safety',
|
||||
updates: {
|
||||
category: 'business-leadership',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'ai-governance-business-case-template-tractatus-framework',
|
||||
updates: {
|
||||
category: 'business-leadership',
|
||||
visibility: 'public',
|
||||
order: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'implementation-roadmap-24-month-deployment-plan',
|
||||
updates: {
|
||||
category: 'business-leadership',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
}
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// ARCHIVE (outdated/superseded)
|
||||
// ========================================
|
||||
{
|
||||
slug: 'phase-5-poc-session-1-summary',
|
||||
updates: {
|
||||
category: 'archived',
|
||||
visibility: 'archived',
|
||||
order: 999
|
||||
}
|
||||
},
|
||||
{
|
||||
slug: 'phase-5-poc-session-2-summary',
|
||||
updates: {
|
||||
category: 'archived',
|
||||
visibility: 'archived',
|
||||
order: 999
|
||||
}
|
||||
},
|
||||
|
||||
// All other Phase 2 documents - stay archived
|
||||
// All session handoffs - stay archived
|
||||
// Test reports - stay archived
|
||||
// Blog outlines - stay archived
|
||||
// Duplicate/superseded documents - stay archived
|
||||
];
|
||||
|
||||
async function migrate() {
|
||||
console.log('🔧 Starting comprehensive document categorization migration...');
|
||||
console.log(` Database: ${DB_NAME}`);
|
||||
console.log('');
|
||||
|
||||
const client = new MongoClient(MONGO_URI);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
|
||||
let updated = 0;
|
||||
let notFound = 0;
|
||||
let unchanged = 0;
|
||||
|
||||
for (const { slug, updates } of DOCUMENT_UPDATES) {
|
||||
const result = await collection.updateOne(
|
||||
{ slug },
|
||||
{ $set: updates }
|
||||
);
|
||||
|
||||
if (result.matchedCount === 0) {
|
||||
console.log(` ⚠️ Document not found: ${slug}`);
|
||||
notFound++;
|
||||
} else if (result.modifiedCount === 0) {
|
||||
console.log(` ℹ️ Already up to date: ${slug}`);
|
||||
unchanged++;
|
||||
} else {
|
||||
console.log(` ✅ Updated: ${slug}`);
|
||||
if (updates.title) {
|
||||
console.log(` - title: ${updates.title}`);
|
||||
}
|
||||
console.log(` - category: ${updates.category}`);
|
||||
console.log(` - visibility: ${updates.visibility}`);
|
||||
console.log(` - order: ${updates.order}`);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('📊 Migration Summary:');
|
||||
console.log(` ✅ Updated: ${updated}`);
|
||||
console.log(` ℹ️ Unchanged: ${unchanged}`);
|
||||
console.log(` ⚠️ Not found: ${notFound}`);
|
||||
console.log('');
|
||||
|
||||
// Verification by category
|
||||
console.log('🔍 Verification - Documents by Category:');
|
||||
const publicDocs = await collection.find({ visibility: 'public' }).sort({ category: 1, order: 1 }).toArray();
|
||||
|
||||
const byCategory = {};
|
||||
publicDocs.forEach(doc => {
|
||||
if (!byCategory[doc.category]) {
|
||||
byCategory[doc.category] = [];
|
||||
}
|
||||
byCategory[doc.category].push(doc);
|
||||
});
|
||||
|
||||
Object.keys(byCategory).sort().forEach(cat => {
|
||||
console.log(`\n ${cat}:`);
|
||||
byCategory[cat].forEach(doc => {
|
||||
console.log(` [${doc.order}] ${doc.title}`);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('✨ Migration complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
|
|
@ -1,392 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Document Migration Script
|
||||
* Migrates markdown documents into the MongoDB database
|
||||
*
|
||||
* Usage:
|
||||
* npm run migrate:docs # Interactive mode
|
||||
* node scripts/migrate-documents.js --source /path/to/docs --dry-run
|
||||
* node scripts/migrate-documents.js --source /path/to/docs --force
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { markdownToHtml, extractTOC, generateSlug } = require('../src/utils/markdown.util');
|
||||
const logger = require('../src/utils/logger.util');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const sourceArg = args.indexOf('--source');
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const force = args.includes('--force');
|
||||
|
||||
// Default source paths
|
||||
const DEFAULT_SOURCES = [
|
||||
'/home/theflow/projects/tractatus/docs/markdown',
|
||||
'/home/theflow/projects/sydigital/stochastic/innovation-exploration/anthropic-submission'
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract front matter from markdown
|
||||
*/
|
||||
function extractFrontMatter(content) {
|
||||
const frontMatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
||||
const match = content.match(frontMatterRegex);
|
||||
|
||||
if (!match) {
|
||||
return { frontMatter: {}, content };
|
||||
}
|
||||
|
||||
const frontMatterText = match[1];
|
||||
const remainingContent = match[2];
|
||||
|
||||
// Parse YAML-like front matter
|
||||
const frontMatter = {};
|
||||
frontMatterText.split('\n').forEach(line => {
|
||||
const [key, ...valueParts] = line.split(':');
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join(':').trim();
|
||||
frontMatter[key.trim()] = value.replace(/^["']|["']$/g, ''); // Remove quotes
|
||||
}
|
||||
});
|
||||
|
||||
return { frontMatter, content: remainingContent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract metadata from filename and content
|
||||
*/
|
||||
function extractMetadata(filename, content, frontMatter) {
|
||||
// Try to extract document identifier from filename
|
||||
// Patterns: TRA-VAL-0001, STO-INN-0010, etc.
|
||||
const identifierMatch = filename.match(/([A-Z]{3}-[A-Z]{3}-\d{4})/);
|
||||
const identifier = identifierMatch ? identifierMatch[1] : null;
|
||||
|
||||
// Extract quadrant from identifier
|
||||
let quadrant = null;
|
||||
if (identifier) {
|
||||
const [quad] = identifier.split('-');
|
||||
const quadrantMap = {
|
||||
'STR': 'strategic',
|
||||
'OPS': 'operational',
|
||||
'TAC': 'tactical',
|
||||
'SYS': 'system',
|
||||
'STO': 'stochastic'
|
||||
};
|
||||
quadrant = quadrantMap[quad] || null;
|
||||
}
|
||||
|
||||
// Extract title from first H1 or front matter
|
||||
let title = frontMatter.title || null;
|
||||
if (!title) {
|
||||
const h1Match = content.match(/^#\s+(.+)$/m);
|
||||
title = h1Match ? h1Match[1] : path.basename(filename, '.md');
|
||||
}
|
||||
|
||||
// Extract version from identifier or front matter
|
||||
let version = frontMatter.version || '1.0';
|
||||
if (identifier && identifier.match(/v(\d+-\d+)/)) {
|
||||
version = identifier.match(/v(\d+-\d+)/)[1].replace('-', '.');
|
||||
}
|
||||
|
||||
// Determine document type
|
||||
let type = frontMatter.type || 'governance';
|
||||
if (filename.includes('technical-proposal')) type = 'technical';
|
||||
else if (filename.includes('appendix')) type = 'technical';
|
||||
else if (filename.includes('framework')) type = 'framework';
|
||||
else if (filename.includes('whitepaper')) type = 'research';
|
||||
else if (filename.includes('case-stud')) type = 'case-study';
|
||||
|
||||
// Extract author
|
||||
const author = frontMatter.author || 'System';
|
||||
|
||||
// Extract tags
|
||||
const tags = frontMatter.tags
|
||||
? frontMatter.tags.split(',').map(t => t.trim())
|
||||
: [];
|
||||
|
||||
return {
|
||||
identifier,
|
||||
title,
|
||||
type,
|
||||
quadrant,
|
||||
version,
|
||||
author,
|
||||
tags,
|
||||
status: 'published'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single markdown file
|
||||
*/
|
||||
async function processMarkdownFile(filePath, sourcePath) {
|
||||
const filename = path.basename(filePath);
|
||||
const rawContent = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
// Extract front matter
|
||||
const { frontMatter, content } = extractFrontMatter(rawContent);
|
||||
|
||||
// Extract metadata
|
||||
const metadata = extractMetadata(filename, content, frontMatter);
|
||||
|
||||
// Convert to HTML
|
||||
const htmlContent = markdownToHtml(content);
|
||||
|
||||
// Extract table of contents
|
||||
const tableOfContents = extractTOC(content);
|
||||
|
||||
// Generate slug from title
|
||||
const slug = generateSlug(metadata.title);
|
||||
|
||||
// Determine if document should be public
|
||||
// Internal document patterns (should NOT be public)
|
||||
const internalPatterns = [
|
||||
'session-handoff',
|
||||
'phase-2',
|
||||
'phase-3',
|
||||
'testing',
|
||||
'progress-report',
|
||||
'blog-post-outlines',
|
||||
'cost-estimates',
|
||||
'deployment-guide',
|
||||
'kickoff-checklist',
|
||||
'preparation-advisory',
|
||||
'soft-launch',
|
||||
'implementation-session',
|
||||
'test-suite'
|
||||
];
|
||||
|
||||
// Check if filename or slug matches internal patterns
|
||||
const isInternal = internalPatterns.some(pattern =>
|
||||
filename.toLowerCase().includes(pattern) ||
|
||||
slug.toLowerCase().includes(pattern)
|
||||
);
|
||||
|
||||
// Determine visibility from front matter or default based on patterns
|
||||
let visibility = frontMatter.visibility || null;
|
||||
|
||||
if (!visibility) {
|
||||
// Check for legacy public field
|
||||
if (frontMatter.public !== undefined) {
|
||||
const isPublic = frontMatter.public === true || frontMatter.public === 'true';
|
||||
visibility = isPublic ? 'public' : 'internal';
|
||||
} else {
|
||||
// Default to internal if matches internal patterns, otherwise public
|
||||
visibility = isInternal ? 'internal' : 'public';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate visibility value
|
||||
const validVisibility = ['public', 'internal', 'confidential', 'archived'];
|
||||
if (!validVisibility.includes(visibility)) {
|
||||
console.warn(`Invalid visibility '${visibility}' for ${filename}, defaulting to 'internal'`);
|
||||
visibility = 'internal';
|
||||
}
|
||||
|
||||
// Determine category from front matter or metadata type
|
||||
const category = frontMatter.category || metadata.type || 'none';
|
||||
|
||||
// Build document object matching Document model schema
|
||||
const doc = {
|
||||
title: metadata.title,
|
||||
slug: slug,
|
||||
quadrant: metadata.quadrant,
|
||||
persistence: 'HIGH', // Default for technical documents
|
||||
visibility: visibility,
|
||||
category: category,
|
||||
content_html: htmlContent,
|
||||
content_markdown: content,
|
||||
toc: tableOfContents,
|
||||
metadata: {
|
||||
author: metadata.author,
|
||||
version: metadata.version,
|
||||
document_code: metadata.identifier,
|
||||
tags: metadata.tags,
|
||||
original_filename: filename,
|
||||
source_path: path.relative(sourcePath, filePath),
|
||||
migrated_at: new Date()
|
||||
},
|
||||
search_index: content.toLowerCase(),
|
||||
translations: {},
|
||||
download_formats: {}
|
||||
};
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all markdown files in directory
|
||||
*/
|
||||
async function findMarkdownFiles(dirPath) {
|
||||
const files = [];
|
||||
|
||||
async function scan(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip node_modules and hidden directories
|
||||
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
||||
await scan(fullPath);
|
||||
}
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
// Skip README files
|
||||
if (!entry.name.toLowerCase().includes('readme')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await scan(dirPath);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
*/
|
||||
async function migrate() {
|
||||
try {
|
||||
console.log('\n=== Tractatus Document Migration ===\n');
|
||||
|
||||
// Determine source path
|
||||
let sourcePath;
|
||||
if (sourceArg !== -1 && args[sourceArg + 1]) {
|
||||
sourcePath = args[sourceArg + 1];
|
||||
} else {
|
||||
// Check default sources
|
||||
for (const defaultPath of DEFAULT_SOURCES) {
|
||||
try {
|
||||
const stat = await fs.stat(defaultPath);
|
||||
if (stat.isDirectory()) {
|
||||
const files = await fs.readdir(defaultPath);
|
||||
if (files.length > 0) {
|
||||
sourcePath = defaultPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Path doesn't exist, try next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourcePath) {
|
||||
console.error('❌ No source path specified and no documents found in default locations.');
|
||||
console.log('\nUsage: npm run migrate:docs -- --source /path/to/docs');
|
||||
console.log('\nDefault locations checked:');
|
||||
DEFAULT_SOURCES.forEach(p => console.log(` - ${p}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📂 Source: ${sourcePath}`);
|
||||
console.log(`🔍 Mode: ${dryRun ? 'DRY RUN (no changes)' : 'MIGRATION (will write to database)'}`);
|
||||
console.log('');
|
||||
|
||||
// Find markdown files
|
||||
const markdownFiles = await findMarkdownFiles(sourcePath);
|
||||
|
||||
if (markdownFiles.length === 0) {
|
||||
console.log('⚠️ No markdown files found.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${markdownFiles.length} markdown file(s):\n`);
|
||||
markdownFiles.forEach((file, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(sourcePath, file)}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
if (!dryRun) {
|
||||
// Connect to database
|
||||
await connect();
|
||||
}
|
||||
|
||||
// Process each file
|
||||
let createdCount = 0;
|
||||
let updatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorsCount = 0;
|
||||
|
||||
for (const filePath of markdownFiles) {
|
||||
try {
|
||||
const doc = await processMarkdownFile(filePath, sourcePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`✓ [DRY RUN] ${filename}`);
|
||||
console.log(` Title: ${doc.title}`);
|
||||
console.log(` Slug: ${doc.slug}`);
|
||||
console.log(` Quadrant: ${doc.quadrant || 'none'}`);
|
||||
console.log(` Code: ${doc.metadata.document_code || 'none'}`);
|
||||
console.log('');
|
||||
createdCount++;
|
||||
} else {
|
||||
// Check if document already exists by slug
|
||||
const existing = await Document.findBySlug(doc.slug);
|
||||
|
||||
if (existing && !force) {
|
||||
console.log(`⊘ SKIPPED ${filename} (already exists: ${existing.slug})`);
|
||||
skippedCount++;
|
||||
} else if (existing && force) {
|
||||
// Update existing document
|
||||
const updatedDoc = await Document.update(existing._id, doc);
|
||||
console.log(`↻ UPDATED ${filename} (${updatedDoc.slug})`);
|
||||
updatedCount++;
|
||||
} else {
|
||||
// Create new document
|
||||
const createdDoc = await Document.create(doc);
|
||||
console.log(`✓ CREATED ${filename} (${createdDoc.slug})`);
|
||||
createdCount++;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`✗ ERROR processing ${path.basename(filePath)}: ${error.message}`);
|
||||
logger.error(`Migration error for ${filePath}:`, error);
|
||||
errorsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Migration Summary ===\n');
|
||||
console.log(` Total files: ${markdownFiles.length}`);
|
||||
console.log(` Created: ${createdCount}`);
|
||||
console.log(` Updated: ${updatedCount}`);
|
||||
console.log(` Skipped: ${skippedCount}`);
|
||||
console.log(` Errors: ${errorsCount}`);
|
||||
console.log('');
|
||||
|
||||
if (dryRun) {
|
||||
console.log('💡 This was a dry run. No changes were made.');
|
||||
console.log(' Run without --dry-run to perform actual migration.');
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
logger.info(`Document migration completed: ${createdCount} created, ${updatedCount} updated, ${skippedCount} skipped, ${errorsCount} errors`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Migration failed:', error.message);
|
||||
logger.error('Migration error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (!dryRun) {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
migrate();
|
||||
}
|
||||
|
||||
module.exports = migrate;
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migrate legacy 'public' field to modern 'visibility' field
|
||||
*
|
||||
* SECURITY: Safe migration with dry-run support
|
||||
* - Migrates public: true → visibility: 'public'
|
||||
* - Migrates public: false → visibility: 'internal'
|
||||
* - Preserves documents that already have visibility set
|
||||
* - Removes the deprecated 'public' field after migration
|
||||
*/
|
||||
|
||||
const { getCollection } = require('../src/utils/db.util');
|
||||
|
||||
async function migrate(dryRun = false) {
|
||||
try {
|
||||
const collection = await getCollection('documents');
|
||||
|
||||
// Find documents with public field but no visibility
|
||||
const docsWithPublicOnly = await collection.find({
|
||||
public: { $exists: true },
|
||||
visibility: { $exists: false }
|
||||
}).toArray();
|
||||
|
||||
// Find documents with both fields (inconsistent state)
|
||||
const docsWithBoth = await collection.find({
|
||||
public: { $exists: true },
|
||||
visibility: { $exists: true }
|
||||
}).toArray();
|
||||
|
||||
console.log('\n📊 Migration Analysis:');
|
||||
console.log(` Documents with only 'public' field: ${docsWithPublicOnly.length}`);
|
||||
console.log(` Documents with both fields: ${docsWithBoth.length}`);
|
||||
console.log(` Total to migrate: ${docsWithPublicOnly.length + docsWithBoth.length}`);
|
||||
|
||||
if (docsWithPublicOnly.length === 0 && docsWithBoth.length === 0) {
|
||||
console.log('\n✅ No documents need migration. All documents already use visibility field.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n🔍 DRY RUN - No changes will be made\n');
|
||||
|
||||
// Show what would be migrated
|
||||
if (docsWithPublicOnly.length > 0) {
|
||||
console.log('Documents with only public field:');
|
||||
docsWithPublicOnly.forEach(doc => {
|
||||
const newVisibility = doc.public ? 'public' : 'internal';
|
||||
console.log(` - ${doc.title} (${doc.slug})`);
|
||||
console.log(` public: ${doc.public} → visibility: '${newVisibility}'`);
|
||||
});
|
||||
}
|
||||
|
||||
if (docsWithBoth.length > 0) {
|
||||
console.log('\nDocuments with both fields (will remove public):');
|
||||
docsWithBoth.forEach(doc => {
|
||||
console.log(` - ${doc.title} (${doc.slug})`);
|
||||
console.log(` current: public=${doc.public}, visibility='${doc.visibility}'`);
|
||||
console.log(` action: Keep visibility='${doc.visibility}', remove public field`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n💡 Run with --execute to perform migration');
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform actual migration
|
||||
console.log('\n🔄 Performing migration...\n');
|
||||
|
||||
let migratedCount = 0;
|
||||
|
||||
// Migrate documents with only public field
|
||||
for (const doc of docsWithPublicOnly) {
|
||||
const visibility = doc.public ? 'public' : 'internal';
|
||||
|
||||
await collection.updateOne(
|
||||
{ _id: doc._id },
|
||||
{
|
||||
$set: { visibility },
|
||||
$unset: { public: "" }
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`✓ ${doc.title}: public=${doc.public} → visibility='${visibility}'`);
|
||||
migratedCount++;
|
||||
}
|
||||
|
||||
// Clean up documents with both fields (keep visibility, remove public)
|
||||
for (const doc of docsWithBoth) {
|
||||
await collection.updateOne(
|
||||
{ _id: doc._id },
|
||||
{ $unset: { public: "" } }
|
||||
);
|
||||
|
||||
console.log(`✓ ${doc.title}: Removed public field, kept visibility='${doc.visibility}'`);
|
||||
migratedCount++;
|
||||
}
|
||||
|
||||
console.log(`\n✅ Migration complete! ${migratedCount} documents updated.`);
|
||||
|
||||
// Verify results
|
||||
const remainingWithPublic = await collection.countDocuments({ public: { $exists: true } });
|
||||
const totalWithVisibility = await collection.countDocuments({ visibility: { $exists: true } });
|
||||
|
||||
console.log('\n📊 Post-migration verification:');
|
||||
console.log(` Documents with 'public' field: ${remainingWithPublic}`);
|
||||
console.log(` Documents with 'visibility' field: ${totalWithVisibility}`);
|
||||
|
||||
if (remainingWithPublic > 0) {
|
||||
console.warn('\n⚠️ Warning: Some documents still have the public field. Review manually.');
|
||||
} else {
|
||||
console.log('\n✅ All documents successfully migrated to visibility field!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = !args.includes('--execute');
|
||||
|
||||
if (dryRun) {
|
||||
console.log('🔍 Running in DRY RUN mode (no changes will be made)');
|
||||
console.log(' Use --execute flag to perform actual migration\n');
|
||||
}
|
||||
|
||||
migrate(dryRun)
|
||||
.then(() => {
|
||||
console.log('\n✨ Script complete');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('\n💥 Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,432 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migration Script: .claude/instruction-history.json → .memory/governance/
|
||||
*
|
||||
* Migrates Tractatus governance rules from legacy .claude/ directory
|
||||
* to new MemoryProxy-managed .memory/governance/ directory.
|
||||
*
|
||||
* Phase 5 PoC - Week 3: Production Migration
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/migrate-to-memory-proxy.js [--dry-run] [--backup]
|
||||
*
|
||||
* Options:
|
||||
* --dry-run Preview migration without making changes
|
||||
* --backup Create backup of source file before migration (default: true)
|
||||
* --force Skip confirmation prompts
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { MemoryProxyService } = require('../src/services/MemoryProxy.service');
|
||||
const logger = require('../src/utils/logger.util');
|
||||
|
||||
// Configuration
|
||||
const SOURCE_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
const BACKUP_DIR = path.join(__dirname, '../.claude/backups');
|
||||
const MEMORY_BASE_PATH = path.join(__dirname, '../.memory');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const isDryRun = args.includes('--dry-run');
|
||||
const createBackup = !args.includes('--no-backup');
|
||||
const forceMode = args.includes('--force');
|
||||
|
||||
/**
|
||||
* Validate source file exists and is readable
|
||||
*/
|
||||
async function validateSource() {
|
||||
try {
|
||||
const stats = await fs.stat(SOURCE_PATH);
|
||||
if (!stats.isFile()) {
|
||||
throw new Error('Source path is not a file');
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new Error(`Source file not found: ${SOURCE_PATH}`);
|
||||
}
|
||||
throw new Error(`Cannot access source file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load rules from source file
|
||||
*/
|
||||
async function loadSourceRules() {
|
||||
try {
|
||||
const data = await fs.readFile(SOURCE_PATH, 'utf8');
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (!parsed.instructions || !Array.isArray(parsed.instructions)) {
|
||||
throw new Error('Invalid source format: missing instructions array');
|
||||
}
|
||||
|
||||
return parsed.instructions;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error(`Invalid JSON in source file: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup of source file
|
||||
*/
|
||||
async function createSourceBackup() {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = path.join(BACKUP_DIR, `instruction-history-${timestamp}.json`);
|
||||
|
||||
try {
|
||||
// Create backup directory if it doesn't exist
|
||||
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
||||
|
||||
// Copy source file to backup
|
||||
await fs.copyFile(SOURCE_PATH, backupPath);
|
||||
|
||||
console.log(` ✓ Backup created: ${backupPath}`);
|
||||
return backupPath;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create backup: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate rules before migration
|
||||
*/
|
||||
function validateRules(rules) {
|
||||
const issues = [];
|
||||
|
||||
rules.forEach((rule, index) => {
|
||||
if (!rule.id) {
|
||||
issues.push(`Rule ${index}: missing 'id' field`);
|
||||
}
|
||||
if (!rule.text) {
|
||||
issues.push(`Rule ${index}: missing 'text' field`);
|
||||
}
|
||||
if (!rule.quadrant) {
|
||||
issues.push(`Rule ${index}: missing 'quadrant' field`);
|
||||
}
|
||||
if (!rule.persistence) {
|
||||
issues.push(`Rule ${index}: missing 'persistence' field`);
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze rules for migration preview
|
||||
*/
|
||||
function analyzeRules(rules) {
|
||||
const analysis = {
|
||||
total: rules.length,
|
||||
by_quadrant: {},
|
||||
by_persistence: {},
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
critical_rules: []
|
||||
};
|
||||
|
||||
rules.forEach(rule => {
|
||||
// Count by quadrant
|
||||
analysis.by_quadrant[rule.quadrant] = (analysis.by_quadrant[rule.quadrant] || 0) + 1;
|
||||
|
||||
// Count by persistence
|
||||
analysis.by_persistence[rule.persistence] = (analysis.by_persistence[rule.persistence] || 0) + 1;
|
||||
|
||||
// Count active/inactive
|
||||
if (rule.active !== false) {
|
||||
analysis.active++;
|
||||
} else {
|
||||
analysis.inactive++;
|
||||
}
|
||||
|
||||
// Identify critical enforcement rules
|
||||
if (['inst_016', 'inst_017', 'inst_018'].includes(rule.id)) {
|
||||
analysis.critical_rules.push(rule.id);
|
||||
}
|
||||
});
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform migration
|
||||
*/
|
||||
async function migrate(rules) {
|
||||
const memoryProxy = new MemoryProxyService({
|
||||
memoryBasePath: MEMORY_BASE_PATH
|
||||
});
|
||||
|
||||
try {
|
||||
// Initialize MemoryProxy
|
||||
await memoryProxy.initialize();
|
||||
console.log(' ✓ MemoryProxy initialized');
|
||||
|
||||
// Persist rules
|
||||
const result = await memoryProxy.persistGovernanceRules(rules);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`Migration failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify migration success
|
||||
*/
|
||||
async function verifyMigration(originalRules) {
|
||||
const memoryProxy = new MemoryProxyService({
|
||||
memoryBasePath: MEMORY_BASE_PATH
|
||||
});
|
||||
|
||||
try {
|
||||
await memoryProxy.initialize();
|
||||
|
||||
// Load rules from memory
|
||||
const migratedRules = await memoryProxy.loadGovernanceRules();
|
||||
|
||||
// Compare counts
|
||||
if (migratedRules.length !== originalRules.length) {
|
||||
throw new Error(`Rule count mismatch: expected ${originalRules.length}, got ${migratedRules.length}`);
|
||||
}
|
||||
|
||||
// Verify critical rules
|
||||
const criticalRuleIds = ['inst_016', 'inst_017', 'inst_018'];
|
||||
for (const ruleId of criticalRuleIds) {
|
||||
const rule = await memoryProxy.getRule(ruleId);
|
||||
if (!rule) {
|
||||
throw new Error(`Critical rule ${ruleId} not found after migration`);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify data integrity for all rules
|
||||
for (let i = 0; i < originalRules.length; i++) {
|
||||
const original = originalRules[i];
|
||||
const migrated = migratedRules.find(r => r.id === original.id);
|
||||
|
||||
if (!migrated) {
|
||||
throw new Error(`Rule ${original.id} missing after migration`);
|
||||
}
|
||||
|
||||
// Check critical fields
|
||||
if (migrated.text !== original.text) {
|
||||
throw new Error(`Rule ${original.id}: text mismatch`);
|
||||
}
|
||||
if (migrated.quadrant !== original.quadrant) {
|
||||
throw new Error(`Rule ${original.id}: quadrant mismatch`);
|
||||
}
|
||||
if (migrated.persistence !== original.persistence) {
|
||||
throw new Error(`Rule ${original.id}: persistence mismatch`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
rulesVerified: migratedRules.length,
|
||||
criticalRulesVerified: criticalRuleIds.length
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Verification failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm migration with user (unless --force)
|
||||
*/
|
||||
async function confirmMigration(analysis) {
|
||||
if (forceMode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('\n⚠️ Migration will:');
|
||||
console.log(` • Copy ${analysis.total} rules to .memory/governance/`);
|
||||
console.log(` • Preserve all rule metadata and fields`);
|
||||
if (createBackup) {
|
||||
console.log(` • Create backup of source file in .claude/backups/`);
|
||||
}
|
||||
console.log('\nContinue? (yes/no): ');
|
||||
|
||||
// Read user input
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
readline.question('', answer => {
|
||||
readline.close();
|
||||
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration workflow
|
||||
*/
|
||||
async function main() {
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(' Tractatus Governance Rules Migration');
|
||||
console.log(' .claude/ → .memory/governance/');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (isDryRun) {
|
||||
console.log('🔍 DRY RUN MODE - No changes will be made\n');
|
||||
}
|
||||
|
||||
const results = {
|
||||
success: false,
|
||||
rulesLoaded: 0,
|
||||
rulesMigrated: 0,
|
||||
backupPath: null,
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Validate source
|
||||
console.log('[Step 1] Validating source file...');
|
||||
await validateSource();
|
||||
console.log(` ✓ Source exists: ${SOURCE_PATH}\n`);
|
||||
|
||||
// Step 2: Load rules
|
||||
console.log('[Step 2] Loading governance rules...');
|
||||
const rules = await loadSourceRules();
|
||||
results.rulesLoaded = rules.length;
|
||||
console.log(` ✓ Loaded ${rules.length} rules\n`);
|
||||
|
||||
// Step 3: Validate rules
|
||||
console.log('[Step 3] Validating rule format...');
|
||||
const validationIssues = validateRules(rules);
|
||||
|
||||
if (validationIssues.length > 0) {
|
||||
console.log(' ✗ Validation issues found:');
|
||||
validationIssues.forEach(issue => console.log(` • ${issue}`));
|
||||
throw new Error('Rule validation failed');
|
||||
}
|
||||
|
||||
console.log(` ✓ All ${rules.length} rules valid\n`);
|
||||
|
||||
// Step 4: Analyze rules
|
||||
console.log('[Step 4] Analyzing rules...');
|
||||
const analysis = analyzeRules(rules);
|
||||
|
||||
console.log(` Total: ${analysis.total} rules`);
|
||||
console.log(` Active: ${analysis.active} | Inactive: ${analysis.inactive}`);
|
||||
console.log('\n By Quadrant:');
|
||||
Object.entries(analysis.by_quadrant).forEach(([quadrant, count]) => {
|
||||
console.log(` ${quadrant}: ${count}`);
|
||||
});
|
||||
console.log('\n By Persistence:');
|
||||
Object.entries(analysis.by_persistence).forEach(([level, count]) => {
|
||||
console.log(` ${level}: ${count}`);
|
||||
});
|
||||
console.log(`\n Critical Rules: ${analysis.critical_rules.join(', ')}\n`);
|
||||
|
||||
// Step 5: Confirm migration
|
||||
if (!isDryRun) {
|
||||
console.log('[Step 5] Confirming migration...');
|
||||
const confirmed = await confirmMigration(analysis);
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('\n❌ Migration cancelled by user\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(' ✓ Migration confirmed\n');
|
||||
} else {
|
||||
console.log('[Step 5] Skipping confirmation (dry-run mode)\n');
|
||||
}
|
||||
|
||||
// Step 6: Create backup
|
||||
if (createBackup && !isDryRun) {
|
||||
console.log('[Step 6] Creating backup...');
|
||||
results.backupPath = await createSourceBackup();
|
||||
console.log();
|
||||
} else if (isDryRun) {
|
||||
console.log('[Step 6] Backup creation (skipped - dry-run)\n');
|
||||
} else {
|
||||
console.log('[Step 6] Backup creation (skipped - --no-backup)\n');
|
||||
}
|
||||
|
||||
// Step 7: Migrate rules
|
||||
if (!isDryRun) {
|
||||
console.log('[Step 7] Migrating rules to MemoryProxy...');
|
||||
const migrationResult = await migrate(rules);
|
||||
results.rulesMigrated = migrationResult.rulesStored;
|
||||
|
||||
console.log(` ✓ Migrated ${migrationResult.rulesStored} rules`);
|
||||
console.log(` Duration: ${migrationResult.duration}ms`);
|
||||
console.log(` Path: ${migrationResult.path}\n`);
|
||||
} else {
|
||||
console.log('[Step 7] Migration (skipped - dry-run)\n');
|
||||
}
|
||||
|
||||
// Step 8: Verify migration
|
||||
if (!isDryRun) {
|
||||
console.log('[Step 8] Verifying migration...');
|
||||
const verification = await verifyMigration(rules);
|
||||
|
||||
console.log(` ✓ Verified ${verification.rulesVerified} rules`);
|
||||
console.log(` ✓ Critical rules: ${verification.criticalRulesVerified}/3\n`);
|
||||
} else {
|
||||
console.log('[Step 8] Verification (skipped - dry-run)\n');
|
||||
}
|
||||
|
||||
results.success = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n✗ MIGRATION FAILED: ${error.message}\n`);
|
||||
if (error.stack && process.env.DEBUG) {
|
||||
console.error('Stack trace:', error.stack);
|
||||
}
|
||||
results.errors.push(error.message);
|
||||
results.success = false;
|
||||
}
|
||||
|
||||
// Results summary
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(' MIGRATION RESULTS');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (results.success) {
|
||||
if (isDryRun) {
|
||||
console.log('✅ DRY RUN SUCCESSFUL - Ready for actual migration');
|
||||
console.log('\nTo perform migration, run:');
|
||||
console.log(' node scripts/migrate-to-memory-proxy.js');
|
||||
} else {
|
||||
console.log('✅ MIGRATION SUCCESSFUL');
|
||||
console.log('\nSummary:');
|
||||
console.log(` • Rules loaded: ${results.rulesLoaded}`);
|
||||
console.log(` • Rules migrated: ${results.rulesMigrated}`);
|
||||
if (results.backupPath) {
|
||||
console.log(` • Backup: ${results.backupPath}`);
|
||||
}
|
||||
console.log('\nNext Steps:');
|
||||
console.log(' 1. Initialize services: await service.initialize()');
|
||||
console.log(' 2. Verify services load rules from .memory/');
|
||||
console.log(' 3. Monitor .memory/audit/ for decision logs');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ MIGRATION FAILED');
|
||||
console.log('\nErrors:');
|
||||
results.errors.forEach(err => console.log(` • ${err}`));
|
||||
}
|
||||
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
process.exit(results.success ? 0 : 1);
|
||||
}
|
||||
|
||||
// Run migration
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,449 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migration Script: Filesystem → MongoDB
|
||||
*
|
||||
* Migrates existing governance rules and audit logs from filesystem to MongoDB
|
||||
*
|
||||
* Sources:
|
||||
* - .claude/instruction-history.json → governanceRules collection
|
||||
* - .memory/audit/decisions-*.jsonl → auditLogs collection
|
||||
*
|
||||
* Safety:
|
||||
* - Dry run mode (preview changes without writing)
|
||||
* - Backup creation before migration
|
||||
* - Validation of data integrity
|
||||
* - Rollback support
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const mongoose = require('mongoose');
|
||||
const GovernanceRule = require('../src/models/GovernanceRule.model');
|
||||
const AuditLog = require('../src/models/AuditLog.model');
|
||||
const logger = require('../src/utils/logger.util');
|
||||
|
||||
// Configuration
|
||||
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
const AUDIT_DIR_PATH = path.join(__dirname, '../.memory/audit');
|
||||
const BACKUP_DIR = path.join(__dirname, '../.migration-backup');
|
||||
|
||||
// Migration statistics
|
||||
const stats = {
|
||||
rulesFound: 0,
|
||||
rulesMigrated: 0,
|
||||
rulesSkipped: 0,
|
||||
auditFilesFound: 0,
|
||||
auditLogsMigrated: 0,
|
||||
auditLogsSkipped: 0,
|
||||
errors: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse instruction history JSON
|
||||
*/
|
||||
async function loadInstructionHistory() {
|
||||
try {
|
||||
const data = await fs.readFile(INSTRUCTION_HISTORY_PATH, 'utf8');
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (!parsed.instructions || !Array.isArray(parsed.instructions)) {
|
||||
throw new Error('Invalid instruction history format');
|
||||
}
|
||||
|
||||
logger.info('Instruction history loaded', {
|
||||
count: parsed.instructions.length,
|
||||
version: parsed.version
|
||||
});
|
||||
|
||||
return parsed.instructions;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.warn('Instruction history file not found', { path: INSTRUCTION_HISTORY_PATH });
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert instruction to governance rule format
|
||||
*/
|
||||
function convertInstructionToRule(instruction) {
|
||||
// Map instruction fields to governance rule schema
|
||||
return {
|
||||
id: instruction.id,
|
||||
text: instruction.text,
|
||||
quadrant: instruction.quadrant,
|
||||
persistence: instruction.persistence,
|
||||
category: instruction.category || 'other',
|
||||
priority: instruction.priority || 50,
|
||||
temporalScope: instruction.temporal_scope || 'PERMANENT',
|
||||
expiresAt: instruction.expires_at ? new Date(instruction.expires_at) : null,
|
||||
active: instruction.active !== false,
|
||||
source: 'migration',
|
||||
createdBy: instruction.created_by || 'migration',
|
||||
examples: instruction.examples || [],
|
||||
relatedRules: instruction.related_rules || [],
|
||||
notes: instruction.notes || ''
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate governance rules to MongoDB
|
||||
*/
|
||||
async function migrateGovernanceRules(dryRun = true) {
|
||||
logger.info('Starting governance rules migration', { dryRun });
|
||||
|
||||
const instructions = await loadInstructionHistory();
|
||||
stats.rulesFound = instructions.length;
|
||||
|
||||
if (instructions.length === 0) {
|
||||
logger.warn('No instructions found to migrate');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const instruction of instructions) {
|
||||
try {
|
||||
const ruleData = convertInstructionToRule(instruction);
|
||||
|
||||
if (dryRun) {
|
||||
logger.info('[DRY RUN] Would create rule', {
|
||||
id: ruleData.id,
|
||||
quadrant: ruleData.quadrant,
|
||||
persistence: ruleData.persistence
|
||||
});
|
||||
stats.rulesMigrated++;
|
||||
} else {
|
||||
// Check if rule already exists
|
||||
const existing = await GovernanceRule.findOne({ id: ruleData.id });
|
||||
|
||||
if (existing) {
|
||||
logger.warn('Rule already exists, skipping', { id: ruleData.id });
|
||||
stats.rulesSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new rule
|
||||
const rule = new GovernanceRule(ruleData);
|
||||
await rule.save();
|
||||
|
||||
logger.info('Rule migrated', {
|
||||
id: ruleData.id,
|
||||
quadrant: ruleData.quadrant
|
||||
});
|
||||
|
||||
stats.rulesMigrated++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to migrate rule', {
|
||||
id: instruction.id,
|
||||
error: error.message
|
||||
});
|
||||
stats.errors.push({
|
||||
type: 'rule',
|
||||
id: instruction.id,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Governance rules migration complete', {
|
||||
found: stats.rulesFound,
|
||||
migrated: stats.rulesMigrated,
|
||||
skipped: stats.rulesSkipped,
|
||||
errors: stats.errors.filter(e => e.type === 'rule').length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load audit logs from JSONL files
|
||||
*/
|
||||
async function loadAuditLogs() {
|
||||
try {
|
||||
const files = await fs.readdir(AUDIT_DIR_PATH);
|
||||
const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
|
||||
|
||||
stats.auditFilesFound = jsonlFiles.length;
|
||||
|
||||
logger.info('Audit log files found', { count: jsonlFiles.length });
|
||||
|
||||
const allLogs = [];
|
||||
|
||||
for (const file of jsonlFiles) {
|
||||
const filePath = path.join(AUDIT_DIR_PATH, file);
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
// Parse JSONL (one JSON object per line)
|
||||
const lines = content.trim().split('\n').filter(line => line.length > 0);
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const log = JSON.parse(line);
|
||||
allLogs.push(log);
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse JSONL line', {
|
||||
file,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Audit logs loaded', { count: allLogs.length });
|
||||
|
||||
return allLogs;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.warn('Audit directory not found', { path: AUDIT_DIR_PATH });
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert audit log to MongoDB format
|
||||
*/
|
||||
function convertAuditLog(log) {
|
||||
return {
|
||||
sessionId: log.sessionId,
|
||||
action: log.action,
|
||||
allowed: log.allowed !== false,
|
||||
rulesChecked: log.rulesChecked || [],
|
||||
violations: (log.violations || []).map(v => ({
|
||||
ruleId: v.ruleId || v,
|
||||
ruleText: v.ruleText || '',
|
||||
severity: v.severity || 'MEDIUM',
|
||||
details: v.details || ''
|
||||
})),
|
||||
metadata: log.metadata || {},
|
||||
domain: log.metadata?.domain || 'UNKNOWN',
|
||||
boundary: log.metadata?.boundary || null,
|
||||
tractatus_section: log.metadata?.tractatus_section || null,
|
||||
service: log.metadata?.service || 'BoundaryEnforcer',
|
||||
durationMs: log.metadata?.durationMs || null,
|
||||
timestamp: log.timestamp ? new Date(log.timestamp) : new Date()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate audit logs to MongoDB
|
||||
*/
|
||||
async function migrateAuditLogs(dryRun = true) {
|
||||
logger.info('Starting audit logs migration', { dryRun });
|
||||
|
||||
const logs = await loadAuditLogs();
|
||||
|
||||
if (logs.length === 0) {
|
||||
logger.warn('No audit logs found to migrate');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const log of logs) {
|
||||
try {
|
||||
const auditData = convertAuditLog(log);
|
||||
|
||||
if (dryRun) {
|
||||
logger.debug('[DRY RUN] Would create audit log', {
|
||||
sessionId: auditData.sessionId,
|
||||
action: auditData.action,
|
||||
allowed: auditData.allowed
|
||||
});
|
||||
stats.auditLogsMigrated++;
|
||||
} else {
|
||||
// Create audit log entry
|
||||
const auditLog = new AuditLog(auditData);
|
||||
await auditLog.save();
|
||||
|
||||
stats.auditLogsMigrated++;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to migrate audit log', {
|
||||
sessionId: log.sessionId,
|
||||
error: error.message
|
||||
});
|
||||
stats.errors.push({
|
||||
type: 'audit',
|
||||
sessionId: log.sessionId,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Audit logs migration complete', {
|
||||
migrated: stats.auditLogsMigrated,
|
||||
errors: stats.errors.filter(e => e.type === 'audit').length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup of filesystem data
|
||||
*/
|
||||
async function createBackup() {
|
||||
logger.info('Creating backup', { dir: BACKUP_DIR });
|
||||
|
||||
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
||||
|
||||
// Backup instruction history
|
||||
try {
|
||||
const historyContent = await fs.readFile(INSTRUCTION_HISTORY_PATH, 'utf8');
|
||||
await fs.writeFile(
|
||||
path.join(BACKUP_DIR, 'instruction-history.json'),
|
||||
historyContent,
|
||||
'utf8'
|
||||
);
|
||||
logger.info('Backed up instruction history');
|
||||
} catch (error) {
|
||||
logger.warn('Could not backup instruction history', { error: error.message });
|
||||
}
|
||||
|
||||
// Backup audit logs
|
||||
try {
|
||||
const auditBackupDir = path.join(BACKUP_DIR, 'audit');
|
||||
await fs.mkdir(auditBackupDir, { recursive: true });
|
||||
|
||||
const files = await fs.readdir(AUDIT_DIR_PATH);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.jsonl')) {
|
||||
const content = await fs.readFile(path.join(AUDIT_DIR_PATH, file), 'utf8');
|
||||
await fs.writeFile(path.join(auditBackupDir, file), content, 'utf8');
|
||||
}
|
||||
}
|
||||
logger.info('Backed up audit logs', { count: files.length });
|
||||
} catch (error) {
|
||||
logger.warn('Could not backup audit logs', { error: error.message });
|
||||
}
|
||||
|
||||
logger.info('Backup complete', { location: BACKUP_DIR });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify migration integrity
|
||||
*/
|
||||
async function verifyMigration() {
|
||||
logger.info('Verifying migration integrity');
|
||||
|
||||
// Count rules in MongoDB
|
||||
const ruleCount = await GovernanceRule.countDocuments({ source: 'migration' });
|
||||
|
||||
// Count audit logs in MongoDB
|
||||
const auditCount = await AuditLog.countDocuments();
|
||||
|
||||
logger.info('Migration verification', {
|
||||
rulesInMongoDB: ruleCount,
|
||||
auditLogsInMongoDB: auditCount,
|
||||
rulesExpected: stats.rulesMigrated,
|
||||
auditLogsExpected: stats.auditLogsMigrated
|
||||
});
|
||||
|
||||
if (ruleCount !== stats.rulesMigrated) {
|
||||
logger.error('Rule count mismatch!', {
|
||||
expected: stats.rulesMigrated,
|
||||
actual: ruleCount
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('✅ Migration verification passed');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main migration function
|
||||
*/
|
||||
async function runMigration(options = {}) {
|
||||
const dryRun = options.dryRun !== false;
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(' Tractatus Migration: Filesystem → MongoDB');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (dryRun) {
|
||||
console.log('⚠️ DRY RUN MODE - No data will be written\n');
|
||||
} else {
|
||||
console.log('🔥 LIVE MODE - Data will be written to MongoDB\n');
|
||||
}
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
logger.info('Connecting to MongoDB');
|
||||
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev');
|
||||
logger.info('MongoDB connected');
|
||||
|
||||
// Create backup (only in live mode)
|
||||
if (!dryRun) {
|
||||
await createBackup();
|
||||
}
|
||||
|
||||
// Migrate governance rules
|
||||
await migrateGovernanceRules(dryRun);
|
||||
|
||||
// Migrate audit logs
|
||||
await migrateAuditLogs(dryRun);
|
||||
|
||||
// Verify migration (only in live mode)
|
||||
if (!dryRun) {
|
||||
await verifyMigration();
|
||||
}
|
||||
|
||||
// Print summary
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(' MIGRATION SUMMARY');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
console.log('Governance Rules:');
|
||||
console.log(` Found: ${stats.rulesFound}`);
|
||||
console.log(` Migrated: ${stats.rulesMigrated}`);
|
||||
console.log(` Skipped: ${stats.rulesSkipped}`);
|
||||
|
||||
console.log('\nAudit Logs:');
|
||||
console.log(` Files: ${stats.auditFilesFound}`);
|
||||
console.log(` Migrated: ${stats.auditLogsMigrated}`);
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.log('\n⚠️ Errors:');
|
||||
stats.errors.forEach(err => {
|
||||
console.log(` - ${err.type}: ${err.id || err.sessionId} - ${err.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n✅ DRY RUN COMPLETE');
|
||||
console.log('\nTo perform actual migration:');
|
||||
console.log(' node scripts/migrate-to-mongodb.js --live\n');
|
||||
} else {
|
||||
console.log('\n✅ MIGRATION COMPLETE');
|
||||
console.log(`\nBackup location: ${BACKUP_DIR}\n`);
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Migration failed', { error: error.message });
|
||||
console.error('\n❌ MIGRATION FAILED:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await mongoose.connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
// CLI execution
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const liveMode = args.includes('--live');
|
||||
|
||||
runMigration({ dryRun: !liveMode })
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { runMigration };
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Migration: Add visibility and order to value pluralism documents
|
||||
*
|
||||
* This migration ensures the 3 value pluralism documents are properly
|
||||
* configured to display in the docs.html sidebar.
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
const MONGO_URI = process.env.MONGODB_URI || process.env.MONGO_URI || 'mongodb://localhost:27017';
|
||||
const DB_NAME = process.env.MONGODB_DB || process.env.MONGO_DB || 'tractatus_prod';
|
||||
|
||||
const UPDATES = [
|
||||
{
|
||||
slug: 'value-pluralism-faq',
|
||||
category: 'getting-started',
|
||||
visibility: 'public',
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
slug: 'pluralistic-values-research-foundations',
|
||||
category: 'research-theory',
|
||||
visibility: 'public',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
slug: 'pluralistic-values-deliberation-plan-v2',
|
||||
category: 'technical-reference',
|
||||
visibility: 'public',
|
||||
order: 12
|
||||
}
|
||||
];
|
||||
|
||||
async function migrate() {
|
||||
console.log('🔧 Starting value pluralism documents migration...');
|
||||
console.log(` Database: ${DB_NAME}`);
|
||||
console.log('');
|
||||
|
||||
const client = new MongoClient(MONGO_URI);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
|
||||
for (const update of UPDATES) {
|
||||
const { slug, ...fields } = update;
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ slug },
|
||||
{ $set: fields }
|
||||
);
|
||||
|
||||
if (result.matchedCount === 0) {
|
||||
console.log(` ⚠️ Document not found: ${slug}`);
|
||||
} else if (result.modifiedCount === 0) {
|
||||
console.log(` ℹ️ Already up to date: ${slug}`);
|
||||
} else {
|
||||
console.log(` ✅ Updated: ${slug}`);
|
||||
console.log(` - category: ${fields.category}`);
|
||||
console.log(` - visibility: ${fields.visibility}`);
|
||||
console.log(` - order: ${fields.order}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Verify
|
||||
console.log('🔍 Verification:');
|
||||
const docs = await collection.find(
|
||||
{ slug: { $in: UPDATES.map(u => u.slug) } },
|
||||
{ projection: { slug: 1, category: 1, visibility: 1, order: 1, _id: 0 } }
|
||||
).toArray();
|
||||
|
||||
docs.forEach(doc => {
|
||||
console.log(` ${doc.slug}:`);
|
||||
console.log(` category: ${doc.category}`);
|
||||
console.log(` visibility: ${doc.visibility}`);
|
||||
console.log(` order: ${doc.order}`);
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('✨ Migration complete!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
migrate();
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
/**
|
||||
* Migration: Enhance Governance Rules Schema
|
||||
* Adds multi-project governance fields to existing rules
|
||||
*
|
||||
* New fields:
|
||||
* - scope (UNIVERSAL | PROJECT_SPECIFIC)
|
||||
* - applicableProjects (array)
|
||||
* - variables (array)
|
||||
* - clarityScore, specificityScore, actionabilityScore
|
||||
* - validationStatus, lastValidated, validationResults
|
||||
* - usageStats
|
||||
* - optimizationHistory
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const GovernanceRule = require('../../src/models/GovernanceRule.model');
|
||||
|
||||
// Database connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
|
||||
async function up() {
|
||||
console.log('🔄 Starting migration: 001-enhance-governance-rules');
|
||||
console.log(' Database:', MONGODB_URI);
|
||||
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log('✓ Connected to MongoDB');
|
||||
|
||||
// Get all existing rules (use lean() to get raw documents without schema defaults)
|
||||
const rules = await GovernanceRule.find({}).lean();
|
||||
console.log(`\n📊 Found ${rules.length} rules to migrate`);
|
||||
|
||||
let updatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const ruleDoc of rules) {
|
||||
// Check if rule already has new fields in database (not just schema defaults)
|
||||
if (ruleDoc.scope !== undefined && ruleDoc.scope !== null) {
|
||||
console.log(` ⏩ Skipping ${ruleDoc.id} (already migrated)`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load the rule with Mongoose model to apply schema methods
|
||||
const rule = await GovernanceRule.findById(ruleDoc._id);
|
||||
|
||||
// Auto-detect variables in rule text (${VAR_NAME} pattern)
|
||||
const variables = [];
|
||||
const varPattern = /\$\{([A-Z_]+)\}/g;
|
||||
let match;
|
||||
while ((match = varPattern.exec(rule.text)) !== null) {
|
||||
if (!variables.includes(match[1])) {
|
||||
variables.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine scope based on variables
|
||||
// If rule has variables, it's likely UNIVERSAL (will be customized per project)
|
||||
// If no variables and rule mentions specific values, it's PROJECT_SPECIFIC
|
||||
const hasVariables = variables.length > 0;
|
||||
const scope = hasVariables ? 'UNIVERSAL' : 'PROJECT_SPECIFIC';
|
||||
|
||||
// Update rule with new fields
|
||||
rule.scope = scope;
|
||||
rule.applicableProjects = ['*']; // Apply to all projects by default
|
||||
rule.variables = variables;
|
||||
|
||||
// Initialize AI optimization fields
|
||||
rule.clarityScore = null; // Will be calculated by AI optimizer
|
||||
rule.specificityScore = null;
|
||||
rule.actionabilityScore = null;
|
||||
rule.lastOptimized = null;
|
||||
rule.optimizationHistory = [];
|
||||
|
||||
// Initialize validation fields
|
||||
rule.validationStatus = 'NOT_VALIDATED';
|
||||
rule.lastValidated = null;
|
||||
rule.validationResults = null;
|
||||
|
||||
// Initialize usage stats
|
||||
rule.usageStats = {
|
||||
referencedInProjects: [],
|
||||
timesEnforced: 0,
|
||||
conflictsDetected: 0,
|
||||
lastEnforced: null
|
||||
};
|
||||
|
||||
await rule.save();
|
||||
|
||||
console.log(` ✓ ${rule.id}: ${scope} (${variables.length} variables)`);
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
console.log(`\n✅ Migration complete!`);
|
||||
console.log(` Updated: ${updatedCount}`);
|
||||
console.log(` Skipped: ${skippedCount}`);
|
||||
console.log(` Total: ${rules.length}`);
|
||||
|
||||
// Create indexes for new fields
|
||||
console.log('\n📊 Creating indexes for new fields...');
|
||||
await GovernanceRule.createIndexes();
|
||||
console.log(' ✓ Indexes created');
|
||||
|
||||
return { success: true, updated: updatedCount, skipped: skippedCount };
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Migration failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
console.log('\n✓ Disconnected from MongoDB');
|
||||
}
|
||||
}
|
||||
|
||||
async function down() {
|
||||
console.log('🔄 Rolling back migration: 001-enhance-governance-rules');
|
||||
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log('✓ Connected to MongoDB');
|
||||
|
||||
// Remove new fields from all rules
|
||||
const result = await GovernanceRule.updateMany(
|
||||
{},
|
||||
{
|
||||
$unset: {
|
||||
scope: '',
|
||||
applicableProjects: '',
|
||||
variables: '',
|
||||
clarityScore: '',
|
||||
specificityScore: '',
|
||||
actionabilityScore: '',
|
||||
lastOptimized: '',
|
||||
optimizationHistory: '',
|
||||
validationStatus: '',
|
||||
lastValidated: '',
|
||||
validationResults: '',
|
||||
usageStats: ''
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`✓ Rollback complete! Removed fields from ${result.modifiedCount} rules`);
|
||||
|
||||
return { success: true, modified: result.modifiedCount };
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Rollback failed:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
console.log('✓ Disconnected from MongoDB');
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
if (require.main === module) {
|
||||
const command = process.argv[2] || 'up';
|
||||
|
||||
if (command === 'up') {
|
||||
up()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
} else if (command === 'down') {
|
||||
down()
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
} else {
|
||||
console.error('Usage: node 001-enhance-governance-rules.js [up|down]');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { up, down };
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Simple CSS minification script
|
||||
* Minifies tractatus-theme.css to tractatus-theme.min.css
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const cssPath = path.join(__dirname, '../public/css/tractatus-theme.css');
|
||||
const minPath = path.join(__dirname, '../public/css/tractatus-theme.min.css');
|
||||
|
||||
// Read CSS
|
||||
const css = fs.readFileSync(cssPath, 'utf8');
|
||||
|
||||
// Simple minification:
|
||||
// 1. Remove comments
|
||||
// 2. Remove extra whitespace
|
||||
// 3. Remove newlines
|
||||
const minified = css
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
|
||||
.replace(/\s+/g, ' ') // Collapse whitespace
|
||||
.replace(/\s*([{}:;,])\s*/g, '$1') // Remove spaces around special chars
|
||||
.replace(/;\}/g, '}') // Remove last semicolon before }
|
||||
.trim();
|
||||
|
||||
// Write minified CSS
|
||||
fs.writeFileSync(minPath, minified);
|
||||
|
||||
console.log(`✓ Minified ${cssPath}`);
|
||||
console.log(` Original: ${css.length} bytes`);
|
||||
console.log(` Minified: ${minified.length} bytes`);
|
||||
console.log(` Saved: ${((1 - minified.length / css.length) * 100).toFixed(1)}%`);
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Mobile Responsiveness Audit
|
||||
*
|
||||
* Checks viewport configuration and touch target sizes (WCAG 2.5.5)
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function success(message) {
|
||||
log(` ✓ ${message}`, 'green');
|
||||
}
|
||||
|
||||
function warning(message) {
|
||||
log(` ⚠ ${message}`, 'yellow');
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
log(` ✗ ${message}`, 'red');
|
||||
}
|
||||
|
||||
// Pages to test
|
||||
const pages = [
|
||||
{ name: 'Homepage', url: 'http://localhost:9000/' },
|
||||
{ name: 'Researcher', url: 'http://localhost:9000/researcher.html' },
|
||||
{ name: 'Implementer', url: 'http://localhost:9000/implementer.html' },
|
||||
{ name: 'Advocate', url: 'http://localhost:9000/advocate.html' },
|
||||
{ name: 'About', url: 'http://localhost:9000/about.html' },
|
||||
{ name: 'Values', url: 'http://localhost:9000/about/values.html' },
|
||||
{ name: 'Docs', url: 'http://localhost:9000/docs.html' },
|
||||
{ name: 'Media Inquiry', url: 'http://localhost:9000/media-inquiry.html' },
|
||||
{ name: 'Case Submission', url: 'http://localhost:9000/case-submission.html' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch page HTML
|
||||
*/
|
||||
function fetchPage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check viewport meta tag
|
||||
*/
|
||||
function checkViewport(html) {
|
||||
const viewportMatch = html.match(/<meta[^>]*name="viewport"[^>]*>/i);
|
||||
|
||||
if (!viewportMatch) {
|
||||
return { exists: false, content: null, valid: false };
|
||||
}
|
||||
|
||||
const contentMatch = viewportMatch[0].match(/content="([^"]*)"/i);
|
||||
const content = contentMatch ? contentMatch[1] : null;
|
||||
|
||||
// Check for proper responsive viewport
|
||||
const hasWidth = content?.includes('width=device-width');
|
||||
const hasInitialScale = content?.includes('initial-scale=1');
|
||||
|
||||
return {
|
||||
exists: true,
|
||||
content,
|
||||
valid: hasWidth && hasInitialScale
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze interactive elements for touch targets
|
||||
*/
|
||||
function analyzeTouchTargets(html) {
|
||||
const issues = [];
|
||||
|
||||
// Check for small buttons (buttons should have min height/width via Tailwind)
|
||||
const buttons = html.match(/<button[^>]*>/g) || [];
|
||||
const buttonClasses = buttons.map(btn => {
|
||||
const classMatch = btn.match(/class="([^"]*)"/);
|
||||
return classMatch ? classMatch[1] : '';
|
||||
});
|
||||
|
||||
// Check for links that might be too small
|
||||
const links = html.match(/<a[^>]*>(?![\s]*<)/g) || [];
|
||||
|
||||
// Check for small padding on interactive elements
|
||||
const smallPadding = buttonClasses.filter(classes =>
|
||||
!classes.includes('p-') && !classes.includes('py-') && !classes.includes('px-')
|
||||
).length;
|
||||
|
||||
if (smallPadding > 0) {
|
||||
issues.push(`${smallPadding} buttons without explicit padding (may be too small)`);
|
||||
}
|
||||
|
||||
// Check for form inputs
|
||||
const inputs = html.match(/<input[^>]*>/g) || [];
|
||||
const inputsWithSmallPadding = inputs.filter(input => {
|
||||
const classMatch = input.match(/class="([^"]*)"/);
|
||||
const classes = classMatch ? classMatch[1] : '';
|
||||
return !classes.includes('p-') && !classes.includes('py-');
|
||||
}).length;
|
||||
|
||||
if (inputsWithSmallPadding > 0) {
|
||||
issues.push(`${inputsWithSmallPadding} form inputs may have insufficient padding`);
|
||||
}
|
||||
|
||||
return {
|
||||
totalButtons: buttons.length,
|
||||
totalLinks: links.length,
|
||||
totalInputs: inputs.length,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for responsive design patterns
|
||||
*/
|
||||
function checkResponsivePatterns(html) {
|
||||
const patterns = {
|
||||
tailwindResponsive: (html.match(/\b(sm:|md:|lg:|xl:|2xl:)/g) || []).length,
|
||||
gridResponsive: (html.match(/grid-cols-1\s+(md:|lg:|xl:)grid-cols-/g) || []).length,
|
||||
flexResponsive: (html.match(/flex-col\s+(sm:|md:|lg:)flex-row/g) || []).length,
|
||||
hideOnMobile: (html.match(/\bhidden\s+(sm:|md:|lg:)block/g) || []).length
|
||||
};
|
||||
|
||||
const totalResponsiveClasses = Object.values(patterns).reduce((a, b) => a + b, 0);
|
||||
|
||||
return {
|
||||
...patterns,
|
||||
totalResponsiveClasses,
|
||||
usesResponsiveDesign: totalResponsiveClasses > 10
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main audit
|
||||
*/
|
||||
async function main() {
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Mobile Responsiveness Audit', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
const results = [];
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const page of pages) {
|
||||
try {
|
||||
const html = await fetchPage(page.url);
|
||||
|
||||
const viewport = checkViewport(html);
|
||||
const touchTargets = analyzeTouchTargets(html);
|
||||
const responsive = checkResponsivePatterns(html);
|
||||
|
||||
const pageResult = {
|
||||
name: page.name,
|
||||
viewport,
|
||||
touchTargets,
|
||||
responsive
|
||||
};
|
||||
|
||||
results.push(pageResult);
|
||||
|
||||
// Display results
|
||||
if (viewport.valid && responsive.usesResponsiveDesign && touchTargets.issues.length === 0) {
|
||||
success(`${page.name.padEnd(20)} Mobile-ready`);
|
||||
passCount++;
|
||||
} else {
|
||||
const issues = [];
|
||||
if (!viewport.valid) issues.push('viewport');
|
||||
if (!responsive.usesResponsiveDesign) issues.push('responsive design');
|
||||
if (touchTargets.issues.length > 0) issues.push('touch targets');
|
||||
|
||||
warning(`${page.name.padEnd(20)} Issues: ${issues.join(', ')}`);
|
||||
touchTargets.issues.forEach(issue => {
|
||||
log(` • ${issue}`, 'yellow');
|
||||
});
|
||||
failCount++;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
error(`${page.name.padEnd(20)} FAILED: ${err.message}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Summary', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
log(` Pages Tested: ${results.length}`, 'bright');
|
||||
success(`Mobile-Ready: ${passCount} pages`);
|
||||
if (failCount > 0) warning(`Needs Improvement: ${failCount} pages`);
|
||||
console.log('');
|
||||
|
||||
// Viewport analysis
|
||||
const withViewport = results.filter(r => r.viewport.exists).length;
|
||||
const validViewport = results.filter(r => r.viewport.valid).length;
|
||||
|
||||
log(' Viewport Meta Tags:', 'bright');
|
||||
success(`${withViewport}/${results.length} pages have viewport meta tag`);
|
||||
if (validViewport < results.length) {
|
||||
warning(`${validViewport}/${results.length} have valid responsive viewport`);
|
||||
} else {
|
||||
success(`${validViewport}/${results.length} have valid responsive viewport`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Responsive design patterns
|
||||
const responsive = results.filter(r => r.responsive.usesResponsiveDesign).length;
|
||||
log(' Responsive Design:', 'bright');
|
||||
if (responsive === results.length) {
|
||||
success(`All pages use responsive design patterns (Tailwind breakpoints)`);
|
||||
} else {
|
||||
warning(`${responsive}/${results.length} pages use sufficient responsive patterns`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Touch target recommendations
|
||||
log(' Recommendations:', 'bright');
|
||||
log(' • All interactive elements should have min 44x44px touch targets (WCAG 2.5.5)', 'cyan');
|
||||
log(' • Buttons: Use px-6 py-3 (Tailwind) for comfortable touch targets', 'cyan');
|
||||
log(' • Links in text: Ensure sufficient line-height and padding', 'cyan');
|
||||
log(' • Form inputs: Use p-3 or py-3 px-4 for easy touch', 'cyan');
|
||||
console.log('');
|
||||
|
||||
// Save report
|
||||
const reportPath = path.join(__dirname, '../audit-reports/mobile-audit-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
pagesТested: results.length,
|
||||
mobileReady: passCount,
|
||||
needsImprovement: failCount,
|
||||
viewportValid: validViewport,
|
||||
responsiveDesign: responsive
|
||||
},
|
||||
results
|
||||
}, null, 2));
|
||||
|
||||
success(`Detailed report saved: ${reportPath}`);
|
||||
console.log('');
|
||||
|
||||
if (failCount === 0) {
|
||||
success('All pages are mobile-ready!');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
} else {
|
||||
warning('Some pages need mobile optimization improvements');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('');
|
||||
error(`Mobile audit failed: ${err.message}`);
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
[Unit]
|
||||
Description=MongoDB Database Server for Tractatus
|
||||
Documentation=https://docs.mongodb.org/manual
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=theflow
|
||||
Group=theflow
|
||||
|
||||
# MongoDB executable and configuration
|
||||
ExecStart=/home/theflow/projects/mongodb/mongodb-server/bin/mongod \
|
||||
--port 27017 \
|
||||
--dbpath /home/theflow/projects/tractatus/data/mongodb \
|
||||
--logpath /home/theflow/projects/tractatus/logs/mongodb.log \
|
||||
--fork \
|
||||
--quiet
|
||||
|
||||
ExecStop=/home/theflow/projects/mongodb/mongodb-server/bin/mongod \
|
||||
--port 27017 \
|
||||
--dbpath /home/theflow/projects/tractatus/data/mongodb \
|
||||
--shutdown
|
||||
|
||||
# Restart policy
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Security settings
|
||||
PrivateTmp=true
|
||||
NoNewPrivileges=true
|
||||
LimitNOFILE=64000
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/home/theflow/projects/tractatus
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Disk Space Monitoring Script
|
||||
# Monitors disk space usage and alerts when thresholds exceeded
|
||||
#
|
||||
# Usage:
|
||||
# ./disk-monitor.sh # Check all monitored paths
|
||||
# ./disk-monitor.sh --test # Test mode (no alerts)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = OK
|
||||
# 1 = Warning threshold exceeded
|
||||
# 2 = Critical threshold exceeded
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
ALERT_EMAIL="${ALERT_EMAIL:-}"
|
||||
LOG_FILE="/var/log/tractatus/disk-monitor.log"
|
||||
WARN_THRESHOLD=80 # Warn at 80% usage
|
||||
CRITICAL_THRESHOLD=90 # Critical at 90% usage
|
||||
|
||||
# Paths to monitor
|
||||
declare -A MONITORED_PATHS=(
|
||||
["/"]="Root filesystem"
|
||||
["/var"]="Var directory"
|
||||
["/var/log"]="Log directory"
|
||||
["/var/www/tractatus"]="Tractatus application"
|
||||
["/tmp"]="Temp directory"
|
||||
)
|
||||
|
||||
# Parse arguments
|
||||
TEST_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--test)
|
||||
TEST_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "[$timestamp] [$level] $message"
|
||||
|
||||
if [[ -d "$(dirname "$LOG_FILE")" ]]; then
|
||||
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Send alert email
|
||||
send_alert() {
|
||||
local subject="$1"
|
||||
local body="$2"
|
||||
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
log "INFO" "TEST MODE: Would send alert: $subject"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$ALERT_EMAIL" ]]; then
|
||||
log "WARN" "No alert email configured (ALERT_EMAIL not set)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v mail &> /dev/null; then
|
||||
echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent to $ALERT_EMAIL"
|
||||
elif command -v sendmail &> /dev/null; then
|
||||
{
|
||||
echo "Subject: $subject"
|
||||
echo "From: tractatus-monitoring@agenticgovernance.digital"
|
||||
echo "To: $ALERT_EMAIL"
|
||||
echo ""
|
||||
echo "$body"
|
||||
} | sendmail "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent via sendmail to $ALERT_EMAIL"
|
||||
else
|
||||
log "WARN" "No email command available"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get disk usage for path
|
||||
get_disk_usage() {
|
||||
local path="$1"
|
||||
|
||||
# Check if path exists
|
||||
if [[ ! -e "$path" ]]; then
|
||||
echo "N/A"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get usage percentage (remove % sign)
|
||||
df -h "$path" 2>/dev/null | awk 'NR==2 {print $5}' | sed 's/%//' || echo "N/A"
|
||||
}
|
||||
|
||||
# Get human-readable disk usage details
|
||||
get_disk_details() {
|
||||
local path="$1"
|
||||
|
||||
if [[ ! -e "$path" ]]; then
|
||||
echo "Path does not exist"
|
||||
return 1
|
||||
fi
|
||||
|
||||
df -h "$path" 2>/dev/null | awk 'NR==2 {printf "Size: %s | Used: %s | Avail: %s | Use%%: %s | Mounted: %s\n", $2, $3, $4, $5, $6}'
|
||||
}
|
||||
|
||||
# Find largest directories in path
|
||||
find_largest_dirs() {
|
||||
local path="$1"
|
||||
local limit="${2:-10}"
|
||||
|
||||
if [[ ! -e "$path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
du -h "$path"/* 2>/dev/null | sort -rh | head -n "$limit" || echo "Unable to scan directory"
|
||||
}
|
||||
|
||||
# Check single path
|
||||
check_path() {
|
||||
local path="$1"
|
||||
local description="$2"
|
||||
|
||||
local usage=$(get_disk_usage "$path")
|
||||
|
||||
if [[ "$usage" == "N/A" ]]; then
|
||||
log "WARN" "$description ($path): Unable to check"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$usage" -ge "$CRITICAL_THRESHOLD" ]]; then
|
||||
log "CRITICAL" "$description ($path): ${usage}% used (>= $CRITICAL_THRESHOLD%)"
|
||||
return 2
|
||||
elif [[ "$usage" -ge "$WARN_THRESHOLD" ]]; then
|
||||
log "WARN" "$description ($path): ${usage}% used (>= $WARN_THRESHOLD%)"
|
||||
return 1
|
||||
else
|
||||
log "INFO" "$description ($path): ${usage}% used"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main monitoring function
|
||||
main() {
|
||||
log "INFO" "Starting disk space monitoring"
|
||||
|
||||
local max_severity=0
|
||||
local issues=()
|
||||
local critical_paths=()
|
||||
local warning_paths=()
|
||||
|
||||
# Check all monitored paths
|
||||
for path in "${!MONITORED_PATHS[@]}"; do
|
||||
local description="${MONITORED_PATHS[$path]}"
|
||||
local exit_code=0
|
||||
|
||||
check_path "$path" "$description" || exit_code=$?
|
||||
|
||||
if [[ "$exit_code" -eq 2 ]]; then
|
||||
max_severity=2
|
||||
critical_paths+=("$path (${description})")
|
||||
elif [[ "$exit_code" -eq 1 ]]; then
|
||||
[[ "$max_severity" -lt 1 ]] && max_severity=1
|
||||
warning_paths+=("$path (${description})")
|
||||
fi
|
||||
done
|
||||
|
||||
# Send alerts if thresholds exceeded
|
||||
if [[ "$max_severity" -eq 2 ]]; then
|
||||
local subject="[CRITICAL] Tractatus Disk Space Critical"
|
||||
local body="CRITICAL: Disk space usage has exceeded ${CRITICAL_THRESHOLD}% on one or more paths.
|
||||
|
||||
Critical Paths (>= ${CRITICAL_THRESHOLD}%):
|
||||
$(printf -- "- %s\n" "${critical_paths[@]}")
|
||||
"
|
||||
|
||||
# Add warning paths if any
|
||||
if [[ "${#warning_paths[@]}" -gt 0 ]]; then
|
||||
body+="
|
||||
Warning Paths (>= ${WARN_THRESHOLD}%):
|
||||
$(printf -- "- %s\n" "${warning_paths[@]}")
|
||||
"
|
||||
fi
|
||||
|
||||
body+="
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
|
||||
Disk Usage Details:
|
||||
$(df -h)
|
||||
|
||||
Largest directories in /var/www/tractatus:
|
||||
$(find_largest_dirs /var/www/tractatus 10)
|
||||
|
||||
Largest log files:
|
||||
$(du -h /var/log/tractatus/*.log 2>/dev/null | sort -rh | head -10 || echo "No log files found")
|
||||
|
||||
Action Required:
|
||||
1. Clean up old log files
|
||||
2. Remove unnecessary files
|
||||
3. Check for runaway processes creating large files
|
||||
4. Consider expanding disk space
|
||||
|
||||
Clean up commands:
|
||||
# Rotate old logs
|
||||
sudo journalctl --vacuum-time=7d
|
||||
|
||||
# Clean up npm cache
|
||||
npm cache clean --force
|
||||
|
||||
# Find large files
|
||||
find /var/www/tractatus -type f -size +100M -exec ls -lh {} \;
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
log "CRITICAL" "Disk space alert sent"
|
||||
|
||||
elif [[ "$max_severity" -eq 1 ]]; then
|
||||
local subject="[WARN] Tractatus Disk Space Warning"
|
||||
local body="WARNING: Disk space usage has exceeded ${WARN_THRESHOLD}% on one or more paths.
|
||||
|
||||
Warning Paths (>= ${WARN_THRESHOLD}%):
|
||||
$(printf -- "- %s\n" "${warning_paths[@]}")
|
||||
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
|
||||
Disk Usage:
|
||||
$(df -h)
|
||||
|
||||
Please review disk usage and clean up if necessary.
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
log "WARN" "Disk space warning sent"
|
||||
else
|
||||
log "INFO" "All monitored paths within acceptable limits"
|
||||
fi
|
||||
|
||||
exit $max_severity
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Health Check Monitoring Script
|
||||
# Monitors Tractatus application health endpoint and service status
|
||||
#
|
||||
# Usage:
|
||||
# ./health-check.sh # Run check, alert if issues
|
||||
# ./health-check.sh --quiet # Suppress output unless error
|
||||
# ./health-check.sh --test # Test mode (no alerts)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = Healthy
|
||||
# 1 = Health endpoint failed
|
||||
# 2 = Service not running
|
||||
# 3 = Configuration error
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
HEALTH_URL="${HEALTH_URL:-https://agenticgovernance.digital/health}"
|
||||
SERVICE_NAME="${SERVICE_NAME:-tractatus}"
|
||||
ALERT_EMAIL="${ALERT_EMAIL:-}"
|
||||
LOG_FILE="/var/log/tractatus/health-check.log"
|
||||
STATE_FILE="/var/tmp/tractatus-health-state"
|
||||
MAX_FAILURES=3 # Alert after 3 consecutive failures
|
||||
|
||||
# Parse arguments
|
||||
QUIET=false
|
||||
TEST_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--quiet) QUIET=true; shift ;;
|
||||
--test) TEST_MODE=true; shift ;;
|
||||
*) echo "Unknown option: $1"; exit 3 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if [[ "$QUIET" != "true" ]] || [[ "$level" == "ERROR" ]] || [[ "$level" == "CRITICAL" ]]; then
|
||||
echo "[$timestamp] [$level] $message"
|
||||
fi
|
||||
|
||||
# Log to file if directory exists
|
||||
if [[ -d "$(dirname "$LOG_FILE")" ]]; then
|
||||
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get current failure count
|
||||
get_failure_count() {
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
cat "$STATE_FILE"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Increment failure count
|
||||
increment_failure_count() {
|
||||
local count=$(get_failure_count)
|
||||
echo $((count + 1)) > "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Reset failure count
|
||||
reset_failure_count() {
|
||||
echo "0" > "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Send alert email
|
||||
send_alert() {
|
||||
local subject="$1"
|
||||
local body="$2"
|
||||
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
log "INFO" "TEST MODE: Would send alert: $subject"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$ALERT_EMAIL" ]]; then
|
||||
log "WARN" "No alert email configured (ALERT_EMAIL not set)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try to send email using mail command (if available)
|
||||
if command -v mail &> /dev/null; then
|
||||
echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent to $ALERT_EMAIL"
|
||||
elif command -v sendmail &> /dev/null; then
|
||||
{
|
||||
echo "Subject: $subject"
|
||||
echo "From: tractatus-monitoring@agenticgovernance.digital"
|
||||
echo "To: $ALERT_EMAIL"
|
||||
echo ""
|
||||
echo "$body"
|
||||
} | sendmail "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent via sendmail to $ALERT_EMAIL"
|
||||
else
|
||||
log "WARN" "No email command available (install mailutils or sendmail)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check health endpoint
|
||||
check_health_endpoint() {
|
||||
log "INFO" "Checking health endpoint: $HEALTH_URL"
|
||||
|
||||
# Make HTTP request with timeout
|
||||
local response
|
||||
local http_code
|
||||
|
||||
response=$(curl -s -w "\n%{http_code}" --max-time 10 "$HEALTH_URL" 2>&1) || {
|
||||
log "ERROR" "Health endpoint request failed: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Extract HTTP code (last line)
|
||||
http_code=$(echo "$response" | tail -n 1)
|
||||
|
||||
# Extract response body (everything except last line)
|
||||
local body=$(echo "$response" | sed '$d')
|
||||
|
||||
# Check HTTP status
|
||||
if [[ "$http_code" != "200" ]]; then
|
||||
log "ERROR" "Health endpoint returned HTTP $http_code"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check response contains expected JSON
|
||||
if ! echo "$body" | jq -e '.status == "ok"' &> /dev/null; then
|
||||
log "ERROR" "Health endpoint response invalid: $body"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "INFO" "Health endpoint OK (HTTP $http_code)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check systemd service status
|
||||
check_service_status() {
|
||||
log "INFO" "Checking service status: $SERVICE_NAME"
|
||||
|
||||
if ! systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
log "ERROR" "Service $SERVICE_NAME is not active"
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Check if service is enabled
|
||||
if ! systemctl is-enabled --quiet "$SERVICE_NAME"; then
|
||||
log "WARN" "Service $SERVICE_NAME is not enabled (won't start on boot)"
|
||||
fi
|
||||
|
||||
log "INFO" "Service $SERVICE_NAME is active"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check database connectivity (quick MongoDB ping)
|
||||
check_database() {
|
||||
log "INFO" "Checking database connectivity"
|
||||
|
||||
# Try to connect to MongoDB (timeout 5 seconds)
|
||||
if ! timeout 5 mongosh --quiet --eval "db.adminCommand('ping')" localhost:27017/tractatus_prod &> /dev/null; then
|
||||
log "ERROR" "Database connection failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "INFO" "Database connectivity OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check disk space
|
||||
check_disk_space() {
|
||||
log "INFO" "Checking disk space"
|
||||
|
||||
# Get root filesystem usage percentage
|
||||
local usage=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
|
||||
|
||||
if [[ "$usage" -gt 90 ]]; then
|
||||
log "CRITICAL" "Disk space critical: ${usage}% used"
|
||||
return 1
|
||||
elif [[ "$usage" -gt 80 ]]; then
|
||||
log "WARN" "Disk space high: ${usage}% used"
|
||||
else
|
||||
log "INFO" "Disk space OK: ${usage}% used"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main health check
|
||||
main() {
|
||||
log "INFO" "Starting health check"
|
||||
|
||||
local all_healthy=true
|
||||
local issues=()
|
||||
|
||||
# Run all checks
|
||||
if ! check_service_status; then
|
||||
all_healthy=false
|
||||
issues+=("Service not running")
|
||||
fi
|
||||
|
||||
if ! check_health_endpoint; then
|
||||
all_healthy=false
|
||||
issues+=("Health endpoint failed")
|
||||
fi
|
||||
|
||||
if ! check_database; then
|
||||
all_healthy=false
|
||||
issues+=("Database connectivity failed")
|
||||
fi
|
||||
|
||||
if ! check_disk_space; then
|
||||
all_healthy=false
|
||||
issues+=("Disk space issue")
|
||||
fi
|
||||
|
||||
# Handle results
|
||||
if [[ "$all_healthy" == "true" ]]; then
|
||||
log "INFO" "All health checks passed ✓"
|
||||
reset_failure_count
|
||||
exit 0
|
||||
else
|
||||
log "ERROR" "Health check failed: ${issues[*]}"
|
||||
increment_failure_count
|
||||
|
||||
local failure_count=$(get_failure_count)
|
||||
log "WARN" "Consecutive failures: $failure_count/$MAX_FAILURES"
|
||||
|
||||
# Alert if threshold reached
|
||||
if [[ "$failure_count" -ge "$MAX_FAILURES" ]]; then
|
||||
local subject="[ALERT] Tractatus Health Check Failed ($failure_count failures)"
|
||||
local body="Tractatus health check has failed $failure_count times consecutively.
|
||||
|
||||
Issues detected:
|
||||
$(printf -- "- %s\n" "${issues[@]}")
|
||||
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
Service: $SERVICE_NAME
|
||||
Health URL: $HEALTH_URL
|
||||
|
||||
Please investigate immediately.
|
||||
|
||||
View logs:
|
||||
sudo journalctl -u $SERVICE_NAME -n 100
|
||||
|
||||
Check service status:
|
||||
sudo systemctl status $SERVICE_NAME
|
||||
|
||||
Restart service:
|
||||
sudo systemctl restart $SERVICE_NAME
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
log "CRITICAL" "Alert sent after $failure_count consecutive failures"
|
||||
fi
|
||||
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Log Monitoring Script
|
||||
# Monitors Tractatus service logs for errors, security events, and anomalies
|
||||
#
|
||||
# Usage:
|
||||
# ./log-monitor.sh # Monitor logs since last check
|
||||
# ./log-monitor.sh --since "1 hour" # Monitor specific time window
|
||||
# ./log-monitor.sh --follow # Continuous monitoring
|
||||
# ./log-monitor.sh --test # Test mode (no alerts)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = No issues found
|
||||
# 1 = Errors detected
|
||||
# 2 = Critical errors detected
|
||||
# 3 = Configuration error
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
SERVICE_NAME="${SERVICE_NAME:-tractatus}"
|
||||
ALERT_EMAIL="${ALERT_EMAIL:-}"
|
||||
LOG_FILE="/var/log/tractatus/log-monitor.log"
|
||||
STATE_FILE="/var/tmp/tractatus-log-monitor-state"
|
||||
ERROR_THRESHOLD=10 # Alert after 10 errors in window
|
||||
CRITICAL_THRESHOLD=3 # Alert immediately after 3 critical errors
|
||||
|
||||
# Parse arguments
|
||||
SINCE="5 minutes ago"
|
||||
FOLLOW=false
|
||||
TEST_MODE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--since)
|
||||
SINCE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--follow)
|
||||
FOLLOW=true
|
||||
shift
|
||||
;;
|
||||
--test)
|
||||
TEST_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "[$timestamp] [$level] $message"
|
||||
|
||||
# Log to file if directory exists
|
||||
if [[ -d "$(dirname "$LOG_FILE")" ]]; then
|
||||
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Send alert email
|
||||
send_alert() {
|
||||
local subject="$1"
|
||||
local body="$2"
|
||||
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
log "INFO" "TEST MODE: Would send alert: $subject"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$ALERT_EMAIL" ]]; then
|
||||
log "WARN" "No alert email configured (ALERT_EMAIL not set)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v mail &> /dev/null; then
|
||||
echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent to $ALERT_EMAIL"
|
||||
elif command -v sendmail &> /dev/null; then
|
||||
{
|
||||
echo "Subject: $subject"
|
||||
echo "From: tractatus-monitoring@agenticgovernance.digital"
|
||||
echo "To: $ALERT_EMAIL"
|
||||
echo ""
|
||||
echo "$body"
|
||||
} | sendmail "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent via sendmail to $ALERT_EMAIL"
|
||||
else
|
||||
log "WARN" "No email command available"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract errors from logs
|
||||
extract_errors() {
|
||||
local since="$1"
|
||||
|
||||
# Get logs since specified time
|
||||
sudo journalctl -u "$SERVICE_NAME" --since "$since" --no-pager 2>/dev/null || {
|
||||
log "ERROR" "Failed to read journal for $SERVICE_NAME"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Analyze log patterns
|
||||
analyze_logs() {
|
||||
local logs="$1"
|
||||
|
||||
# Count different severity levels (grep -c returns 0 if no matches, no need for fallback)
|
||||
local error_count=$(echo "$logs" | grep -ci "\[ERROR\]" || true)
|
||||
[[ -z "$error_count" ]] && error_count=0
|
||||
|
||||
local critical_count=$(echo "$logs" | grep -ci "\[CRITICAL\]" || true)
|
||||
[[ -z "$critical_count" ]] && critical_count=0
|
||||
|
||||
local warn_count=$(echo "$logs" | grep -ci "\[WARN\]" || true)
|
||||
[[ -z "$warn_count" ]] && warn_count=0
|
||||
|
||||
# Security-related patterns
|
||||
local security_count=$(echo "$logs" | grep -ciE "(SECURITY|unauthorized|forbidden|authentication failed)" || true)
|
||||
[[ -z "$security_count" ]] && security_count=0
|
||||
|
||||
# Database errors
|
||||
local db_error_count=$(echo "$logs" | grep -ciE "(mongodb|database|connection.*failed)" || true)
|
||||
[[ -z "$db_error_count" ]] && db_error_count=0
|
||||
|
||||
# HTTP errors
|
||||
local http_error_count=$(echo "$logs" | grep -ciE "HTTP.*50[0-9]|Internal Server Error" || true)
|
||||
[[ -z "$http_error_count" ]] && http_error_count=0
|
||||
|
||||
# Unhandled exceptions
|
||||
local exception_count=$(echo "$logs" | grep -ciE "(Unhandled.*exception|TypeError|ReferenceError)" || true)
|
||||
[[ -z "$exception_count" ]] && exception_count=0
|
||||
|
||||
log "INFO" "Log analysis: CRITICAL=$critical_count ERROR=$error_count WARN=$warn_count SECURITY=$security_count DB_ERROR=$db_error_count HTTP_ERROR=$http_error_count EXCEPTION=$exception_count"
|
||||
|
||||
# Determine severity
|
||||
if [[ "$critical_count" -ge "$CRITICAL_THRESHOLD" ]]; then
|
||||
log "CRITICAL" "Critical error threshold exceeded: $critical_count critical errors"
|
||||
return 2
|
||||
fi
|
||||
|
||||
if [[ "$error_count" -ge "$ERROR_THRESHOLD" ]]; then
|
||||
log "ERROR" "Error threshold exceeded: $error_count errors"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$security_count" -gt 0 ]]; then
|
||||
log "WARN" "Security events detected: $security_count events"
|
||||
fi
|
||||
|
||||
if [[ "$db_error_count" -gt 5 ]]; then
|
||||
log "WARN" "Database errors detected: $db_error_count errors"
|
||||
fi
|
||||
|
||||
if [[ "$exception_count" -gt 0 ]]; then
|
||||
log "WARN" "Unhandled exceptions detected: $exception_count exceptions"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extract top error messages
|
||||
get_top_errors() {
|
||||
local logs="$1"
|
||||
local limit="${2:-10}"
|
||||
|
||||
echo "$logs" | grep -iE "\[ERROR\]|\[CRITICAL\]" | \
|
||||
sed 's/^.*\] //' | \
|
||||
sort | uniq -c | sort -rn | head -n "$limit"
|
||||
}
|
||||
|
||||
# Main monitoring function
|
||||
main() {
|
||||
log "INFO" "Starting log monitoring (since: $SINCE)"
|
||||
|
||||
# Extract logs
|
||||
local logs
|
||||
logs=$(extract_errors "$SINCE") || {
|
||||
log "ERROR" "Failed to extract logs"
|
||||
exit 3
|
||||
}
|
||||
|
||||
# Count total log entries
|
||||
local log_count=$(echo "$logs" | wc -l)
|
||||
log "INFO" "Analyzing $log_count log entries"
|
||||
|
||||
if [[ "$log_count" -eq 0 ]]; then
|
||||
log "INFO" "No logs found in time window"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Analyze logs
|
||||
local exit_code=0
|
||||
analyze_logs "$logs" || exit_code=$?
|
||||
|
||||
# If errors detected, send alert
|
||||
if [[ "$exit_code" -ne 0 ]]; then
|
||||
local severity="ERROR"
|
||||
[[ "$exit_code" -eq 2 ]] && severity="CRITICAL"
|
||||
|
||||
local subject="[ALERT] Tractatus Log Monitoring - $severity Detected"
|
||||
|
||||
# Extract top 10 error messages
|
||||
local top_errors=$(get_top_errors "$logs" 10)
|
||||
|
||||
local body="Log monitoring detected $severity level issues in Tractatus service.
|
||||
|
||||
Time Window: $SINCE
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
Service: $SERVICE_NAME
|
||||
|
||||
Top Error Messages:
|
||||
$top_errors
|
||||
|
||||
Recent Critical/Error Logs:
|
||||
$(echo "$logs" | grep -iE "\[ERROR\]|\[CRITICAL\]" | tail -n 20)
|
||||
|
||||
Full logs:
|
||||
sudo journalctl -u $SERVICE_NAME --since \"$SINCE\"
|
||||
|
||||
Check service status:
|
||||
sudo systemctl status $SERVICE_NAME
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
else
|
||||
log "INFO" "No significant issues detected"
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
# Follow mode (continuous monitoring)
|
||||
follow_logs() {
|
||||
log "INFO" "Starting continuous log monitoring"
|
||||
|
||||
sudo journalctl -u "$SERVICE_NAME" -f --no-pager | while read -r line; do
|
||||
# Check for error patterns
|
||||
if echo "$line" | grep -qiE "\[ERROR\]|\[CRITICAL\]"; then
|
||||
log "ERROR" "$line"
|
||||
|
||||
# Extract error message
|
||||
local error_msg=$(echo "$line" | sed 's/^.*\] //')
|
||||
|
||||
# Check for critical patterns
|
||||
if echo "$line" | grep -qiE "\[CRITICAL\]|Unhandled.*exception|Database.*failed|Service.*crashed"; then
|
||||
local subject="[CRITICAL] Tractatus Error Detected"
|
||||
local body="Critical error detected in Tractatus logs:
|
||||
|
||||
$line
|
||||
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
|
||||
Recent logs:
|
||||
$(sudo journalctl -u $SERVICE_NAME -n 10 --no-pager)
|
||||
"
|
||||
send_alert "$subject" "$body"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Run appropriate mode
|
||||
if [[ "$FOLLOW" == "true" ]]; then
|
||||
follow_logs
|
||||
else
|
||||
main
|
||||
fi
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Master Monitoring Script
|
||||
# Orchestrates all monitoring checks for Tractatus production environment
|
||||
#
|
||||
# Usage:
|
||||
# ./monitor-all.sh # Run all monitors
|
||||
# ./monitor-all.sh --test # Test mode (no alerts)
|
||||
# ./monitor-all.sh --skip-ssl # Skip SSL check
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = All checks passed
|
||||
# 1 = Some warnings
|
||||
# 2 = Some critical issues
|
||||
# 3 = Configuration error
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOG_FILE="/var/log/tractatus/monitoring.log"
|
||||
ALERT_EMAIL="${ALERT_EMAIL:-}"
|
||||
|
||||
# Parse arguments
|
||||
TEST_MODE=false
|
||||
SKIP_SSL=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--test)
|
||||
TEST_MODE=true
|
||||
shift
|
||||
;;
|
||||
--skip-ssl)
|
||||
SKIP_SSL=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Export configuration for child scripts
|
||||
export ALERT_EMAIL
|
||||
[[ "$TEST_MODE" == "true" ]] && TEST_FLAG="--test" || TEST_FLAG=""
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "[$timestamp] [$level] $message"
|
||||
|
||||
if [[ -d "$(dirname "$LOG_FILE")" ]]; then
|
||||
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run monitoring check
|
||||
run_check() {
|
||||
local name="$1"
|
||||
local script="$2"
|
||||
shift 2
|
||||
local args="$@"
|
||||
|
||||
log "INFO" "Running $name..."
|
||||
|
||||
local exit_code=0
|
||||
"$SCRIPT_DIR/$script" $args $TEST_FLAG || exit_code=$?
|
||||
|
||||
case $exit_code in
|
||||
0)
|
||||
log "INFO" "$name: OK ✓"
|
||||
;;
|
||||
1)
|
||||
log "WARN" "$name: Warning"
|
||||
;;
|
||||
2)
|
||||
log "CRITICAL" "$name: Critical"
|
||||
;;
|
||||
*)
|
||||
log "ERROR" "$name: Error (exit code: $exit_code)"
|
||||
;;
|
||||
esac
|
||||
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Main monitoring function
|
||||
main() {
|
||||
log "INFO" "=== Starting Tractatus Monitoring Suite ==="
|
||||
log "INFO" "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
||||
log "INFO" "Host: $(hostname)"
|
||||
[[ "$TEST_MODE" == "true" ]] && log "INFO" "TEST MODE: Alerts suppressed"
|
||||
|
||||
local max_severity=0
|
||||
local checks_run=0
|
||||
local checks_passed=0
|
||||
local checks_warned=0
|
||||
local checks_critical=0
|
||||
local checks_failed=0
|
||||
|
||||
# Health Check
|
||||
if run_check "Health Check" "health-check.sh"; then
|
||||
((checks_passed++))
|
||||
else
|
||||
local exit_code=$?
|
||||
[[ $exit_code -eq 1 ]] && ((checks_warned++))
|
||||
[[ $exit_code -eq 2 ]] && ((checks_critical++))
|
||||
[[ $exit_code -ge 3 ]] && ((checks_failed++))
|
||||
[[ $exit_code -gt $max_severity ]] && max_severity=$exit_code
|
||||
fi
|
||||
((checks_run++))
|
||||
|
||||
# Log Monitor
|
||||
if run_check "Log Monitor" "log-monitor.sh" --since "5 minutes ago"; then
|
||||
((checks_passed++))
|
||||
else
|
||||
local exit_code=$?
|
||||
[[ $exit_code -eq 1 ]] && ((checks_warned++))
|
||||
[[ $exit_code -eq 2 ]] && ((checks_critical++))
|
||||
[[ $exit_code -ge 3 ]] && ((checks_failed++))
|
||||
[[ $exit_code -gt $max_severity ]] && max_severity=$exit_code
|
||||
fi
|
||||
((checks_run++))
|
||||
|
||||
# Disk Monitor
|
||||
if run_check "Disk Monitor" "disk-monitor.sh"; then
|
||||
((checks_passed++))
|
||||
else
|
||||
local exit_code=$?
|
||||
[[ $exit_code -eq 1 ]] && ((checks_warned++))
|
||||
[[ $exit_code -eq 2 ]] && ((checks_critical++))
|
||||
[[ $exit_code -ge 3 ]] && ((checks_failed++))
|
||||
[[ $exit_code -gt $max_severity ]] && max_severity=$exit_code
|
||||
fi
|
||||
((checks_run++))
|
||||
|
||||
# SSL Monitor (optional)
|
||||
if [[ "$SKIP_SSL" != "true" ]]; then
|
||||
if run_check "SSL Monitor" "ssl-monitor.sh"; then
|
||||
((checks_passed++))
|
||||
else
|
||||
local exit_code=$?
|
||||
[[ $exit_code -eq 1 ]] && ((checks_warned++))
|
||||
[[ $exit_code -eq 2 ]] && ((checks_critical++))
|
||||
[[ $exit_code -ge 3 ]] && ((checks_failed++))
|
||||
[[ $exit_code -gt $max_severity ]] && max_severity=$exit_code
|
||||
fi
|
||||
((checks_run++))
|
||||
fi
|
||||
|
||||
# Summary
|
||||
log "INFO" "=== Monitoring Summary ==="
|
||||
log "INFO" "Checks run: $checks_run"
|
||||
log "INFO" "Passed: $checks_passed | Warned: $checks_warned | Critical: $checks_critical | Failed: $checks_failed"
|
||||
|
||||
if [[ $max_severity -eq 0 ]]; then
|
||||
log "INFO" "All monitoring checks passed ✓"
|
||||
elif [[ $max_severity -eq 1 ]]; then
|
||||
log "WARN" "Some checks returned warnings"
|
||||
elif [[ $max_severity -eq 2 ]]; then
|
||||
log "CRITICAL" "Some checks returned critical alerts"
|
||||
else
|
||||
log "ERROR" "Some checks failed"
|
||||
fi
|
||||
|
||||
log "INFO" "=== Monitoring Complete ==="
|
||||
|
||||
exit $max_severity
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# SSL Certificate Monitoring Script
|
||||
# Monitors SSL certificate expiry and alerts before expiration
|
||||
#
|
||||
# Usage:
|
||||
# ./ssl-monitor.sh # Check all domains
|
||||
# ./ssl-monitor.sh --domain example.com # Check specific domain
|
||||
# ./ssl-monitor.sh --test # Test mode (no alerts)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = OK
|
||||
# 1 = Warning (expires soon)
|
||||
# 2 = Critical (expires very soon)
|
||||
# 3 = Expired or error
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
ALERT_EMAIL="${ALERT_EMAIL:-}"
|
||||
LOG_FILE="/var/log/tractatus/ssl-monitor.log"
|
||||
WARN_DAYS=30 # Warn 30 days before expiry
|
||||
CRITICAL_DAYS=7 # Critical alert 7 days before expiry
|
||||
|
||||
# Default domains to monitor
|
||||
DOMAINS=(
|
||||
"agenticgovernance.digital"
|
||||
)
|
||||
|
||||
# Parse arguments
|
||||
TEST_MODE=false
|
||||
SPECIFIC_DOMAIN=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--domain)
|
||||
SPECIFIC_DOMAIN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--test)
|
||||
TEST_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Override domains if specific domain provided
|
||||
if [[ -n "$SPECIFIC_DOMAIN" ]]; then
|
||||
DOMAINS=("$SPECIFIC_DOMAIN")
|
||||
fi
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "[$timestamp] [$level] $message"
|
||||
|
||||
if [[ -d "$(dirname "$LOG_FILE")" ]]; then
|
||||
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Send alert email
|
||||
send_alert() {
|
||||
local subject="$1"
|
||||
local body="$2"
|
||||
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
log "INFO" "TEST MODE: Would send alert: $subject"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$ALERT_EMAIL" ]]; then
|
||||
log "WARN" "No alert email configured (ALERT_EMAIL not set)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v mail &> /dev/null; then
|
||||
echo "$body" | mail -s "$subject" "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent to $ALERT_EMAIL"
|
||||
elif command -v sendmail &> /dev/null; then
|
||||
{
|
||||
echo "Subject: $subject"
|
||||
echo "From: tractatus-monitoring@agenticgovernance.digital"
|
||||
echo "To: $ALERT_EMAIL"
|
||||
echo ""
|
||||
echo "$body"
|
||||
} | sendmail "$ALERT_EMAIL"
|
||||
log "INFO" "Alert email sent via sendmail to $ALERT_EMAIL"
|
||||
else
|
||||
log "WARN" "No email command available"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get SSL certificate expiry date
|
||||
get_cert_expiry() {
|
||||
local domain="$1"
|
||||
|
||||
# Use openssl to get certificate
|
||||
local expiry_date
|
||||
expiry_date=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \
|
||||
openssl x509 -noout -enddate 2>/dev/null | \
|
||||
cut -d= -f2) || {
|
||||
log "ERROR" "Failed to retrieve certificate for $domain"
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "$expiry_date"
|
||||
}
|
||||
|
||||
# Get days until expiry
|
||||
get_days_until_expiry() {
|
||||
local expiry_date="$1"
|
||||
|
||||
# Convert expiry date to seconds since epoch
|
||||
local expiry_epoch
|
||||
expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null) || {
|
||||
log "ERROR" "Failed to parse expiry date: $expiry_date"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get current time in seconds since epoch
|
||||
local now_epoch=$(date +%s)
|
||||
|
||||
# Calculate days until expiry
|
||||
local seconds_until_expiry=$((expiry_epoch - now_epoch))
|
||||
local days_until_expiry=$((seconds_until_expiry / 86400))
|
||||
|
||||
echo "$days_until_expiry"
|
||||
}
|
||||
|
||||
# Get certificate details
|
||||
get_cert_details() {
|
||||
local domain="$1"
|
||||
|
||||
echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null | \
|
||||
openssl x509 -noout -subject -issuer -dates 2>/dev/null || {
|
||||
echo "Failed to retrieve certificate details"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Check single domain
|
||||
check_domain() {
|
||||
local domain="$1"
|
||||
|
||||
log "INFO" "Checking SSL certificate for $domain"
|
||||
|
||||
# Get expiry date
|
||||
local expiry_date
|
||||
expiry_date=$(get_cert_expiry "$domain") || {
|
||||
log "ERROR" "Failed to check certificate for $domain"
|
||||
return 3
|
||||
}
|
||||
|
||||
# Calculate days until expiry
|
||||
local days_until_expiry
|
||||
days_until_expiry=$(get_days_until_expiry "$expiry_date") || {
|
||||
log "ERROR" "Failed to calculate expiry for $domain"
|
||||
return 3
|
||||
}
|
||||
|
||||
# Check if expired
|
||||
if [[ "$days_until_expiry" -lt 0 ]]; then
|
||||
log "CRITICAL" "$domain: Certificate EXPIRED ${days_until_expiry#-} days ago!"
|
||||
return 3
|
||||
fi
|
||||
|
||||
# Check thresholds
|
||||
if [[ "$days_until_expiry" -le "$CRITICAL_DAYS" ]]; then
|
||||
log "CRITICAL" "$domain: Certificate expires in $days_until_expiry days (expires: $expiry_date)"
|
||||
return 2
|
||||
elif [[ "$days_until_expiry" -le "$WARN_DAYS" ]]; then
|
||||
log "WARN" "$domain: Certificate expires in $days_until_expiry days (expires: $expiry_date)"
|
||||
return 1
|
||||
else
|
||||
log "INFO" "$domain: Certificate valid for $days_until_expiry days (expires: $expiry_date)"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main monitoring function
|
||||
main() {
|
||||
log "INFO" "Starting SSL certificate monitoring"
|
||||
|
||||
local max_severity=0
|
||||
local expired_domains=()
|
||||
local critical_domains=()
|
||||
local warning_domains=()
|
||||
|
||||
# Check all domains
|
||||
for domain in "${DOMAINS[@]}"; do
|
||||
local exit_code=0
|
||||
local expiry_date=$(get_cert_expiry "$domain" 2>/dev/null || echo "Unknown")
|
||||
local days_until_expiry=$(get_days_until_expiry "$expiry_date" 2>/dev/null || echo "Unknown")
|
||||
|
||||
check_domain "$domain" || exit_code=$?
|
||||
|
||||
if [[ "$exit_code" -eq 3 ]]; then
|
||||
max_severity=3
|
||||
expired_domains+=("$domain (EXPIRED or ERROR)")
|
||||
elif [[ "$exit_code" -eq 2 ]]; then
|
||||
[[ "$max_severity" -lt 2 ]] && max_severity=2
|
||||
critical_domains+=("$domain (expires in $days_until_expiry days)")
|
||||
elif [[ "$exit_code" -eq 1 ]]; then
|
||||
[[ "$max_severity" -lt 1 ]] && max_severity=1
|
||||
warning_domains+=("$domain (expires in $days_until_expiry days)")
|
||||
fi
|
||||
done
|
||||
|
||||
# Send alerts based on severity
|
||||
if [[ "$max_severity" -eq 3 ]]; then
|
||||
local subject="[CRITICAL] SSL Certificate Expired or Error"
|
||||
local body="CRITICAL: SSL certificate has expired or error occurred.
|
||||
|
||||
Expired/Error Domains:
|
||||
$(printf -- "- %s\n" "${expired_domains[@]}")
|
||||
"
|
||||
|
||||
# Add other alerts if any
|
||||
if [[ "${#critical_domains[@]}" -gt 0 ]]; then
|
||||
body+="
|
||||
Critical Domains (<= $CRITICAL_DAYS days):
|
||||
$(printf -- "- %s\n" "${critical_domains[@]}")
|
||||
"
|
||||
fi
|
||||
|
||||
if [[ "${#warning_domains[@]}" -gt 0 ]]; then
|
||||
body+="
|
||||
Warning Domains (<= $WARN_DAYS days):
|
||||
$(printf -- "- %s\n" "${warning_domains[@]}")
|
||||
"
|
||||
fi
|
||||
|
||||
body+="
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
|
||||
Action Required:
|
||||
1. Renew SSL certificate immediately
|
||||
2. Check Let's Encrypt auto-renewal:
|
||||
sudo certbot renew --dry-run
|
||||
|
||||
Certificate details:
|
||||
$(get_cert_details "${DOMAINS[0]}")
|
||||
|
||||
Renewal commands:
|
||||
# Test renewal
|
||||
sudo certbot renew --dry-run
|
||||
|
||||
# Force renewal
|
||||
sudo certbot renew --force-renewal
|
||||
|
||||
# Check certificate status
|
||||
sudo certbot certificates
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
log "CRITICAL" "SSL certificate alert sent"
|
||||
|
||||
elif [[ "$max_severity" -eq 2 ]]; then
|
||||
local subject="[CRITICAL] SSL Certificate Expires Soon"
|
||||
local body="CRITICAL: SSL certificate expires in $CRITICAL_DAYS days or less.
|
||||
|
||||
Critical Domains (<= $CRITICAL_DAYS days):
|
||||
$(printf -- "- %s\n" "${critical_domains[@]}")
|
||||
"
|
||||
|
||||
if [[ "${#warning_domains[@]}" -gt 0 ]]; then
|
||||
body+="
|
||||
Warning Domains (<= $WARN_DAYS days):
|
||||
$(printf -- "- %s\n" "${warning_domains[@]}")
|
||||
"
|
||||
fi
|
||||
|
||||
body+="
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
|
||||
Please renew certificates soon.
|
||||
|
||||
Check renewal:
|
||||
sudo certbot renew --dry-run
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
log "CRITICAL" "SSL expiry alert sent"
|
||||
|
||||
elif [[ "$max_severity" -eq 1 ]]; then
|
||||
local subject="[WARN] SSL Certificate Expires Soon"
|
||||
local body="WARNING: SSL certificate expires in $WARN_DAYS days or less.
|
||||
|
||||
Warning Domains (<= $WARN_DAYS days):
|
||||
$(printf -- "- %s\n" "${warning_domains[@]}")
|
||||
|
||||
Time: $(date '+%Y-%m-%d %H:%M:%S %Z')
|
||||
Host: $(hostname)
|
||||
|
||||
Please plan certificate renewal.
|
||||
"
|
||||
|
||||
send_alert "$subject" "$body"
|
||||
log "WARN" "SSL expiry warning sent"
|
||||
else
|
||||
log "INFO" "All SSL certificates valid"
|
||||
fi
|
||||
|
||||
exit $max_severity
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Parse Architectural Safeguards Document into Sections
|
||||
* Updates database with section metadata for card-based rendering
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { parseDocumentSections } = require('../src/utils/document-section-parser');
|
||||
const { markdownToHtml } = require('../src/utils/markdown.util');
|
||||
|
||||
async function parseAndUpdateDocument() {
|
||||
try {
|
||||
console.log('\n=== Parsing Architectural Safeguards Document ===\n');
|
||||
|
||||
const mdPath = path.resolve('docs/research/ARCHITECTURAL-SAFEGUARDS-Against-LLM-Hierarchical-Dominance-Prose.md');
|
||||
const slug = 'architectural-safeguards-against-llm-hierarchical-dominance-prose';
|
||||
|
||||
// Read markdown file
|
||||
console.log('📄 Reading markdown file...');
|
||||
const rawContent = await fs.readFile(mdPath, 'utf-8');
|
||||
|
||||
// Parse into sections
|
||||
console.log('🔍 Parsing document into sections...');
|
||||
const sections = parseDocumentSections(rawContent);
|
||||
|
||||
console.log(`✓ Found ${sections.length} sections`);
|
||||
|
||||
// Convert each section's markdown to HTML
|
||||
console.log('🔄 Converting sections to HTML...');
|
||||
sections.forEach(section => {
|
||||
section.content_html = markdownToHtml(section.content);
|
||||
});
|
||||
|
||||
console.log('✓ Converted all sections to HTML');
|
||||
|
||||
// Connect to database
|
||||
await connect();
|
||||
|
||||
// Find existing document
|
||||
console.log('📊 Finding document in database...');
|
||||
const doc = await Document.findBySlug(slug);
|
||||
|
||||
if (!doc) {
|
||||
console.error('❌ Error: Document not found in database');
|
||||
await close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✓ Found document');
|
||||
|
||||
// Update document with sections and move to getting-started
|
||||
console.log('💾 Updating document with sections...');
|
||||
|
||||
const updated = await Document.update(doc._id, {
|
||||
sections: sections,
|
||||
category: 'getting-started',
|
||||
order: 2 // Prominently placed in getting-started
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
console.error('❌ Error: Document update failed');
|
||||
await close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✓ Document updated successfully!');
|
||||
|
||||
console.log('\n📊 Document Details:');
|
||||
console.log(` Title: ${doc.title}`);
|
||||
console.log(` Slug: ${doc.slug}`);
|
||||
console.log(` Category: getting-started (moved from research-theory)`);
|
||||
console.log(` Order: 2`);
|
||||
console.log(` Sections: ${sections.length}`);
|
||||
|
||||
console.log('\n📋 Section Breakdown:');
|
||||
const categoryCounts = {};
|
||||
sections.forEach(section => {
|
||||
categoryCounts[section.category] = (categoryCounts[section.category] || 0) + 1;
|
||||
});
|
||||
|
||||
Object.entries(categoryCounts).forEach(([category, count]) => {
|
||||
console.log(` ${category}: ${count} sections`);
|
||||
});
|
||||
|
||||
console.log('\n✅ Document now has card-based rendering enabled!');
|
||||
console.log(` View at: https://agenticgovernance.digital/docs.html?doc=${slug}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Update failed:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
parseAndUpdateDocument();
|
||||
}
|
||||
|
||||
module.exports = parseAndUpdateDocument;
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Performance Audit Script
|
||||
*
|
||||
* Tests page load times and identifies optimization opportunities
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
function log(message, color = 'reset') {
|
||||
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function success(message) {
|
||||
log(` ✓ ${message}`, 'green');
|
||||
}
|
||||
|
||||
function warning(message) {
|
||||
log(` ⚠ ${message}`, 'yellow');
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
log(` ✗ ${message}`, 'red');
|
||||
}
|
||||
|
||||
// Pages to test
|
||||
const pages = [
|
||||
{ name: 'Homepage', url: 'http://localhost:9000/' },
|
||||
{ name: 'Researcher', url: 'http://localhost:9000/researcher.html' },
|
||||
{ name: 'Implementer', url: 'http://localhost:9000/implementer.html' },
|
||||
{ name: 'Advocate', url: 'http://localhost:9000/advocate.html' },
|
||||
{ name: 'About', url: 'http://localhost:9000/about.html' },
|
||||
{ name: 'Values', url: 'http://localhost:9000/about/values.html' },
|
||||
{ name: 'Docs', url: 'http://localhost:9000/docs.html' },
|
||||
{ name: 'Media Inquiry', url: 'http://localhost:9000/media-inquiry.html' },
|
||||
{ name: 'Case Submission', url: 'http://localhost:9000/case-submission.html' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch a page and measure load time
|
||||
*/
|
||||
function fetchPage(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
http.get(url, (res) => {
|
||||
let data = '';
|
||||
let firstByteTime = null;
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
if (!firstByteTime) {
|
||||
firstByteTime = Date.now() - startTime;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
const totalTime = Date.now() - startTime;
|
||||
const size = Buffer.byteLength(data, 'utf8');
|
||||
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
firstByteTime,
|
||||
totalTime,
|
||||
size,
|
||||
data
|
||||
});
|
||||
});
|
||||
|
||||
}).on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze HTML for optimization opportunities
|
||||
*/
|
||||
function analyzeHTML(html) {
|
||||
const issues = [];
|
||||
|
||||
// Check for inline scripts
|
||||
const inlineScriptMatches = html.match(/<script(?![^>]*src=)[^>]*>/g) || [];
|
||||
if (inlineScriptMatches.length > 3) {
|
||||
issues.push(`Many inline scripts (${inlineScriptMatches.length}) - consider bundling`);
|
||||
}
|
||||
|
||||
// Check for large inline styles
|
||||
const styleMatches = html.match(/<style[^>]*>([\s\S]*?)<\/style>/g) || [];
|
||||
const totalStyleLength = styleMatches.reduce((sum, style) => sum + style.length, 0);
|
||||
if (totalStyleLength > 5000) {
|
||||
issues.push(`Large inline styles (${(totalStyleLength / 1024).toFixed(1)}KB) - consider external CSS`);
|
||||
}
|
||||
|
||||
// Check for unoptimized images
|
||||
const imgMatches = html.match(/<img[^>]*>/g) || [];
|
||||
const imgsWithoutAlt = imgMatches.filter(img => !img.includes('alt=')).length;
|
||||
if (imgsWithoutAlt > 0) {
|
||||
issues.push(`${imgsWithoutAlt} images without alt attributes`);
|
||||
}
|
||||
|
||||
// Check for external resources
|
||||
const externalCSS = (html.match(/<link[^>]*rel="stylesheet"[^>]*>/g) || []).length;
|
||||
const externalJS = (html.match(/<script[^>]*src=[^>]*>/g) || []).length;
|
||||
|
||||
return {
|
||||
inlineScripts: inlineScriptMatches.length,
|
||||
totalStyleLength,
|
||||
images: imgMatches.length,
|
||||
externalCSS,
|
||||
externalJS,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main audit
|
||||
*/
|
||||
async function main() {
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Performance Audit', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
const results = [];
|
||||
let totalTime = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const page of pages) {
|
||||
try {
|
||||
const result = await fetchPage(page.url);
|
||||
const analysis = analyzeHTML(result.data);
|
||||
|
||||
results.push({
|
||||
name: page.name,
|
||||
url: page.url,
|
||||
...result,
|
||||
...analysis
|
||||
});
|
||||
|
||||
totalTime += result.totalTime;
|
||||
totalSize += result.size;
|
||||
|
||||
// Display result
|
||||
const sizeKB = (result.size / 1024).toFixed(1);
|
||||
const timeStatus = result.totalTime < 100 ? 'green' : result.totalTime < 500 ? 'yellow' : 'red';
|
||||
|
||||
log(`${page.name.padEnd(20)} ${result.totalTime}ms ${sizeKB}KB`, timeStatus);
|
||||
|
||||
if (analysis.issues.length > 0) {
|
||||
analysis.issues.forEach(issue => {
|
||||
log(` • ${issue}`, 'yellow');
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
error(`${page.name.padEnd(20)} FAILED: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
log(' Summary', 'bright');
|
||||
log('═'.repeat(70), 'cyan');
|
||||
console.log('');
|
||||
|
||||
const avgTime = (totalTime / results.length).toFixed(0);
|
||||
const avgSize = (totalSize / results.length / 1024).toFixed(1);
|
||||
|
||||
log(` Pages Tested: ${results.length}`, 'bright');
|
||||
log(` Average Load Time: ${avgTime}ms`, avgTime < 200 ? 'green' : avgTime < 500 ? 'yellow' : 'red');
|
||||
log(` Average Page Size: ${avgSize}KB`, avgSize < 50 ? 'green' : avgSize < 100 ? 'yellow' : 'red');
|
||||
log(` Total Size: ${(totalSize / 1024).toFixed(1)}KB`, 'cyan');
|
||||
console.log('');
|
||||
|
||||
// Performance grades
|
||||
const fast = results.filter(r => r.totalTime < 200).length;
|
||||
const medium = results.filter(r => r.totalTime >= 200 && r.totalTime < 500).length;
|
||||
const slow = results.filter(r => r.totalTime >= 500).length;
|
||||
|
||||
success(`Fast (<200ms): ${fast} pages`);
|
||||
if (medium > 0) warning(`Medium (200-500ms): ${medium} pages`);
|
||||
if (slow > 0) error(`Slow (>500ms): ${slow} pages`);
|
||||
console.log('');
|
||||
|
||||
// Recommendations
|
||||
log(' Recommendations:', 'bright');
|
||||
const allIssues = results.flatMap(r => r.issues);
|
||||
|
||||
if (allIssues.length === 0) {
|
||||
success('No major performance issues detected!');
|
||||
} else {
|
||||
// Group similar issues
|
||||
const issueGroups = {};
|
||||
allIssues.forEach(issue => {
|
||||
const key = issue.split('(')[0].trim();
|
||||
issueGroups[key] = (issueGroups[key] || 0) + 1;
|
||||
});
|
||||
|
||||
Object.entries(issueGroups).forEach(([issue, count]) => {
|
||||
log(` • ${issue} (${count} pages)`, 'yellow');
|
||||
});
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Save detailed report
|
||||
const reportPath = path.join(__dirname, '../audit-reports/performance-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
pagesТested: results.length,
|
||||
averageLoadTime: parseInt(avgTime),
|
||||
averageSize: parseFloat(avgSize),
|
||||
fast, medium, slow
|
||||
},
|
||||
results
|
||||
}, null, 2));
|
||||
|
||||
success(`Detailed report saved: ${reportPath}`);
|
||||
console.log('');
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('');
|
||||
error(`Performance audit failed: ${err.message}`);
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,457 +0,0 @@
|
|||
/**
|
||||
* Plan Reminder System
|
||||
* Scans for plan documents, tracks status, and reminds about reviews
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const PLAN_REGISTRY = path.join(__dirname, '../.claude/plan-registry.json');
|
||||
const PROJECT_ROOT = path.join(__dirname, '..');
|
||||
|
||||
// Only search within tractatus directory, exclude other projects
|
||||
const SEARCH_DIRS = [
|
||||
path.join(PROJECT_ROOT, 'docs/plans'),
|
||||
path.join(PROJECT_ROOT, 'docs'),
|
||||
path.join(PROJECT_ROOT, 'docs/research'),
|
||||
path.join(PROJECT_ROOT, 'docs/planning'),
|
||||
path.join(PROJECT_ROOT, 'docs/governance')
|
||||
];
|
||||
|
||||
// Exclude these directories and projects
|
||||
const EXCLUDE_PATTERNS = [
|
||||
/node_modules/,
|
||||
/\.git/,
|
||||
/sydigital/,
|
||||
/passport-consolidated/,
|
||||
/family-history/,
|
||||
/mysy/
|
||||
];
|
||||
|
||||
// Plan document patterns
|
||||
const PLAN_PATTERNS = [
|
||||
/plan.*\.md$/i,
|
||||
/roadmap.*\.md$/i,
|
||||
/session.*handoff.*\.md$/i,
|
||||
/priority.*\.md$/i,
|
||||
/-plan\.md$/i
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse plan metadata from markdown
|
||||
*/
|
||||
function parsePlanMetadata(content, filepath) {
|
||||
const metadata = {
|
||||
filepath: filepath,
|
||||
filename: path.basename(filepath),
|
||||
title: null,
|
||||
status: null,
|
||||
priority: null,
|
||||
created: null,
|
||||
due: null,
|
||||
review_schedule: null,
|
||||
next_review: null,
|
||||
owner: null,
|
||||
completeness: null,
|
||||
last_modified: null
|
||||
};
|
||||
|
||||
// Extract title (first H1)
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
if (titleMatch) {
|
||||
metadata.title = titleMatch[1].trim();
|
||||
}
|
||||
|
||||
// Extract metadata fields
|
||||
const statusMatch = content.match(/\*\*Status:\*\*\s*(.+)/i);
|
||||
if (statusMatch) {
|
||||
metadata.status = statusMatch[1].trim();
|
||||
}
|
||||
|
||||
const priorityMatch = content.match(/\*\*Priority:\*\*\s*(.+)/i);
|
||||
if (priorityMatch) {
|
||||
metadata.priority = priorityMatch[1].trim();
|
||||
}
|
||||
|
||||
const createdMatch = content.match(/\*\*(?:Plan Created|Created):\*\*\s*(.+)/i);
|
||||
if (createdMatch) {
|
||||
metadata.created = createdMatch[1].trim();
|
||||
}
|
||||
|
||||
const dueMatch = content.match(/\*\*(?:Target Completion|Due):\*\*\s*(.+)/i);
|
||||
if (dueMatch) {
|
||||
metadata.due = dueMatch[1].trim();
|
||||
}
|
||||
|
||||
const reviewMatch = content.match(/\*\*Review Schedule:\*\*\s*(.+)/i);
|
||||
if (reviewMatch) {
|
||||
metadata.review_schedule = reviewMatch[1].trim();
|
||||
}
|
||||
|
||||
const nextReviewMatch = content.match(/\*\*Next (?:Review|review):\*\*\s*(.+)/i);
|
||||
if (nextReviewMatch) {
|
||||
metadata.next_review = nextReviewMatch[1].trim();
|
||||
}
|
||||
|
||||
const ownerMatch = content.match(/\*\*(?:Plan )?Owner:\*\*\s*(.+)/i);
|
||||
if (ownerMatch) {
|
||||
metadata.owner = ownerMatch[1].trim();
|
||||
}
|
||||
|
||||
// Analyze completeness based on checkboxes
|
||||
const totalCheckboxes = (content.match(/\[[ x✓]\]/gi) || []).length;
|
||||
const checkedBoxes = (content.match(/\[[x✓]\]/gi) || []).length;
|
||||
|
||||
if (totalCheckboxes > 0) {
|
||||
metadata.completeness = {
|
||||
total: totalCheckboxes,
|
||||
completed: checkedBoxes,
|
||||
percentage: Math.round((checkedBoxes / totalCheckboxes) * 100)
|
||||
};
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan directories for plan documents
|
||||
*/
|
||||
async function scanForPlans() {
|
||||
const plans = [];
|
||||
|
||||
for (const dir of SEARCH_DIRS) {
|
||||
try {
|
||||
const items = await fs.readdir(dir, { recursive: true, withFileTypes: true });
|
||||
|
||||
for (const item of items) {
|
||||
if (!item.isFile()) continue;
|
||||
|
||||
const filename = item.name;
|
||||
const filepath = path.join(item.path || dir, filename);
|
||||
|
||||
// Skip if matches exclusion patterns
|
||||
if (EXCLUDE_PATTERNS.some(pattern => pattern.test(filepath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isPlan = PLAN_PATTERNS.some(pattern => pattern.test(filename));
|
||||
|
||||
if (isPlan) {
|
||||
try {
|
||||
const content = await fs.readFile(filepath, 'utf-8');
|
||||
const stats = await fs.stat(filepath);
|
||||
|
||||
const metadata = parsePlanMetadata(content, filepath);
|
||||
metadata.last_modified = stats.mtime.toISOString();
|
||||
metadata.file_size = stats.size;
|
||||
|
||||
plans.push(metadata);
|
||||
} catch (err) {
|
||||
console.error(` ✗ Error reading ${filepath}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Directory might not exist, skip
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return plans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate review urgency
|
||||
*/
|
||||
function calculateUrgency(plan) {
|
||||
if (!plan.next_review) return 'unknown';
|
||||
|
||||
try {
|
||||
const nextReview = new Date(plan.next_review);
|
||||
const now = new Date();
|
||||
const daysUntil = Math.ceil((nextReview - now) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysUntil < 0) return 'overdue';
|
||||
if (daysUntil === 0) return 'today';
|
||||
if (daysUntil <= 3) return 'this-week';
|
||||
if (daysUntil <= 14) return 'soon';
|
||||
return 'scheduled';
|
||||
} catch (err) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine plan health
|
||||
*/
|
||||
function assessPlanHealth(plan) {
|
||||
const issues = [];
|
||||
const now = new Date();
|
||||
|
||||
// Check if status is active but stale (>30 days since last modified)
|
||||
if (plan.status && plan.status.toLowerCase().includes('active')) {
|
||||
const lastMod = new Date(plan.last_modified);
|
||||
const daysSinceUpdate = (now - lastMod) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (daysSinceUpdate > 30) {
|
||||
issues.push(`Stale: No updates in ${Math.round(daysSinceUpdate)} days`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if completion is low but due date approaching
|
||||
if (plan.completeness && plan.due) {
|
||||
try {
|
||||
const dueDate = new Date(plan.due);
|
||||
const daysUntilDue = (dueDate - now) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (daysUntilDue < 14 && plan.completeness.percentage < 50) {
|
||||
issues.push(`At risk: ${plan.completeness.percentage}% complete, due in ${Math.round(daysUntilDue)} days`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Invalid date, skip
|
||||
}
|
||||
}
|
||||
|
||||
// Check if review is overdue
|
||||
const urgency = calculateUrgency(plan);
|
||||
if (urgency === 'overdue') {
|
||||
issues.push('Review overdue');
|
||||
}
|
||||
|
||||
// Check if no owner assigned
|
||||
if (!plan.owner || plan.owner.includes('TBD') || plan.owner.includes('assigned')) {
|
||||
issues.push('No owner assigned');
|
||||
}
|
||||
|
||||
// Overall health assessment
|
||||
if (issues.length === 0) return { status: 'healthy', issues: [] };
|
||||
if (issues.length === 1) return { status: 'attention', issues };
|
||||
return { status: 'critical', issues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update plan registry
|
||||
*/
|
||||
async function updateRegistry(plans) {
|
||||
// Deduplicate plans by filepath
|
||||
const uniquePlans = [];
|
||||
const seenPaths = new Set();
|
||||
|
||||
for (const plan of plans) {
|
||||
if (!seenPaths.has(plan.filepath)) {
|
||||
seenPaths.add(plan.filepath);
|
||||
uniquePlans.push(plan);
|
||||
}
|
||||
}
|
||||
|
||||
const registry = {
|
||||
version: '1.0.0',
|
||||
last_scan: new Date().toISOString(),
|
||||
total_plans: uniquePlans.length,
|
||||
plans: uniquePlans.map(plan => ({
|
||||
...plan,
|
||||
urgency: calculateUrgency(plan),
|
||||
health: assessPlanHealth(plan)
|
||||
}))
|
||||
};
|
||||
|
||||
await fs.writeFile(PLAN_REGISTRY, JSON.stringify(registry, null, 2));
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display plan reminders
|
||||
*/
|
||||
function displayReminders(registry) {
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(' Plan Reminder System');
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
|
||||
console.log(`Last Scan: ${new Date(registry.last_scan).toLocaleString()}`);
|
||||
console.log(`Total Plans: ${registry.total_plans}\n`);
|
||||
|
||||
// Group plans by urgency
|
||||
const overdue = registry.plans.filter(p => p.urgency === 'overdue');
|
||||
const today = registry.plans.filter(p => p.urgency === 'today');
|
||||
const thisWeek = registry.plans.filter(p => p.urgency === 'this-week');
|
||||
const critical = registry.plans.filter(p => p.health.status === 'critical');
|
||||
const attention = registry.plans.filter(p => p.health.status === 'attention');
|
||||
|
||||
// Display overdue reviews
|
||||
if (overdue.length > 0) {
|
||||
console.log('🔴 OVERDUE REVIEWS:');
|
||||
overdue.forEach(plan => {
|
||||
console.log(` • ${plan.title || plan.filename}`);
|
||||
console.log(` Status: ${plan.status || 'Unknown'}`);
|
||||
console.log(` Next Review: ${plan.next_review}`);
|
||||
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
|
||||
if (plan.health.issues.length > 0) {
|
||||
plan.health.issues.forEach(issue => console.log(` ⚠ ${issue}`));
|
||||
}
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Display today's reviews
|
||||
if (today.length > 0) {
|
||||
console.log('🟡 REVIEWS DUE TODAY:');
|
||||
today.forEach(plan => {
|
||||
console.log(` • ${plan.title || plan.filename}`);
|
||||
console.log(` Status: ${plan.status || 'Unknown'}`);
|
||||
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Display this week's reviews
|
||||
if (thisWeek.length > 0) {
|
||||
console.log('🟢 REVIEWS THIS WEEK:');
|
||||
thisWeek.forEach(plan => {
|
||||
console.log(` • ${plan.title || plan.filename}`);
|
||||
console.log(` Next Review: ${plan.next_review}`);
|
||||
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Display critical health issues
|
||||
if (critical.length > 0) {
|
||||
console.log('🚨 PLANS NEEDING ATTENTION:');
|
||||
critical.forEach(plan => {
|
||||
console.log(` • ${plan.title || plan.filename}`);
|
||||
console.log(` Status: ${plan.status || 'Unknown'}`);
|
||||
plan.health.issues.forEach(issue => console.log(` ⚠ ${issue}`));
|
||||
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Display plans needing attention (not critical)
|
||||
if (attention.length > 0 && critical.length === 0) {
|
||||
console.log('ℹ️ PLANS WITH MINOR ISSUES:');
|
||||
attention.forEach(plan => {
|
||||
console.log(` • ${plan.title || plan.filename}`);
|
||||
plan.health.issues.forEach(issue => console.log(` • ${issue}`));
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log('SUMMARY:');
|
||||
console.log(` Overdue Reviews: ${overdue.length}`);
|
||||
console.log(` Due Today: ${today.length}`);
|
||||
console.log(` Due This Week: ${thisWeek.length}`);
|
||||
console.log(` Critical Health: ${critical.length}`);
|
||||
console.log(` Needs Attention: ${attention.length}`);
|
||||
console.log(` Healthy: ${registry.plans.filter(p => p.health.status === 'healthy').length}`);
|
||||
console.log('═══════════════════════════════════════════════════════════\n');
|
||||
|
||||
// Recommendations
|
||||
if (overdue.length > 0 || critical.length > 0) {
|
||||
console.log('📌 RECOMMENDED ACTIONS:');
|
||||
if (overdue.length > 0) {
|
||||
console.log(' 1. Review overdue plans and update next_review dates');
|
||||
}
|
||||
if (critical.length > 0) {
|
||||
console.log(' 2. Address critical health issues (stale plans, missing owners)');
|
||||
}
|
||||
console.log(' 3. Update plan status and completeness checkboxes');
|
||||
console.log(' 4. Run this reminder weekly to stay on track\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all plans
|
||||
*/
|
||||
function listAllPlans(registry, options = {}) {
|
||||
console.log('\n📋 ALL TRACKED PLANS:\n');
|
||||
|
||||
const sortedPlans = [...registry.plans];
|
||||
|
||||
// Sort by status priority
|
||||
const statusPriority = {
|
||||
'active': 1,
|
||||
'in progress': 2,
|
||||
'pending': 3,
|
||||
'on hold': 4,
|
||||
'completed': 5,
|
||||
'cancelled': 6
|
||||
};
|
||||
|
||||
sortedPlans.sort((a, b) => {
|
||||
const aStatus = (a.status || '').toLowerCase();
|
||||
const bStatus = (b.status || '').toLowerCase();
|
||||
const aPriority = statusPriority[aStatus] || 99;
|
||||
const bPriority = statusPriority[bStatus] || 99;
|
||||
return aPriority - bPriority;
|
||||
});
|
||||
|
||||
sortedPlans.forEach((plan, index) => {
|
||||
console.log(`${index + 1}. ${plan.title || plan.filename}`);
|
||||
console.log(` Status: ${plan.status || 'Unknown'} | Priority: ${plan.priority || 'Unknown'}`);
|
||||
|
||||
if (plan.completeness) {
|
||||
console.log(` Completeness: ${plan.completeness.completed}/${plan.completeness.total} (${plan.completeness.percentage}%)`);
|
||||
}
|
||||
|
||||
if (plan.next_review) {
|
||||
console.log(` Next Review: ${plan.next_review} (${plan.urgency})`);
|
||||
}
|
||||
|
||||
if (plan.owner) {
|
||||
console.log(` Owner: ${plan.owner}`);
|
||||
}
|
||||
|
||||
if (plan.health.status !== 'healthy') {
|
||||
console.log(` Health: ${plan.health.status}`);
|
||||
if (options.verbose) {
|
||||
plan.health.issues.forEach(issue => console.log(` • ${issue}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` File: ${path.relative(process.cwd(), plan.filepath)}`);
|
||||
console.log('');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0] || 'remind';
|
||||
|
||||
try {
|
||||
console.log('\nScanning for plan documents...');
|
||||
const plans = await scanForPlans();
|
||||
console.log(`✓ Found ${plans.length} plan documents\n`);
|
||||
|
||||
const registry = await updateRegistry(plans);
|
||||
|
||||
if (command === 'list') {
|
||||
listAllPlans(registry, { verbose: args.includes('--verbose') });
|
||||
} else {
|
||||
displayReminders(registry);
|
||||
}
|
||||
|
||||
// Exit with code based on urgency
|
||||
if (registry.plans.some(p => p.urgency === 'overdue')) {
|
||||
process.exit(1); // Overdue plans found
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
if (args.includes('--verbose')) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { scanForPlans, updateRegistry, calculateUrgency, assessPlanHealth };
|
||||
|
|
@ -1,442 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Pre-Action Check - Blocking Validator for Major Operations
|
||||
*
|
||||
* This script MUST be called before any major action in a Claude Code session.
|
||||
* It validates that appropriate Tractatus framework components have been used.
|
||||
*
|
||||
* CRITICAL: This is a Claude Code-specific enforcement mechanism.
|
||||
*
|
||||
* Major actions include:
|
||||
* - File modifications (Edit, Write)
|
||||
* - Database schema changes
|
||||
* - Architecture decisions
|
||||
* - Configuration changes
|
||||
* - Security implementations
|
||||
*
|
||||
* Exit Codes:
|
||||
* 0 - PASS: All checks passed, action may proceed
|
||||
* 1 - FAIL: Required checks missing, action blocked
|
||||
* 2 - ERROR: System error, cannot validate
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SESSION_STATE_PATH = path.join(__dirname, '../.claude/session-state.json');
|
||||
const TOKEN_CHECKPOINTS_PATH = path.join(__dirname, '../.claude/token-checkpoints.json');
|
||||
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
green: '\x1b[32m',
|
||||
cyan: '\x1b[36m',
|
||||
bold: '\x1b[1m'
|
||||
};
|
||||
|
||||
// Parse command-line arguments
|
||||
const args = process.argv.slice(2);
|
||||
const actionType = args[0] || 'general';
|
||||
let filePath = null;
|
||||
let actionDescription = 'unspecified action';
|
||||
|
||||
// Check if second argument is a file path
|
||||
if (args.length > 1) {
|
||||
const potentialPath = args[1];
|
||||
if (potentialPath.includes('/') || potentialPath.includes('\\') || potentialPath.endsWith('.html') || potentialPath.endsWith('.js')) {
|
||||
filePath = potentialPath;
|
||||
actionDescription = args.slice(2).join(' ') || `action on ${filePath}`;
|
||||
} else {
|
||||
actionDescription = args.slice(1).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
function log(level, message) {
|
||||
const prefix = {
|
||||
INFO: `${colors.cyan}[PRE-ACTION CHECK]${colors.reset}`,
|
||||
PASS: `${colors.green}${colors.bold}[✓ PASS]${colors.reset}`,
|
||||
FAIL: `${colors.red}${colors.bold}[✗ FAIL]${colors.reset}`,
|
||||
WARN: `${colors.yellow}[⚠ WARN]${colors.reset}`,
|
||||
ERROR: `${colors.red}[ERROR]${colors.reset}`
|
||||
}[level] || '[CHECK]';
|
||||
|
||||
console.log(`${prefix} ${message}`);
|
||||
}
|
||||
|
||||
function loadJSON(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (error) {
|
||||
log('ERROR', `Failed to load ${filePath}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function checkPressureRecent(state, maxTokensAgo = 25000) {
|
||||
const activity = state.last_framework_activity.ContextPressureMonitor;
|
||||
const tokensSince = state.token_estimate - activity.tokens;
|
||||
|
||||
if (tokensSince > maxTokensAgo) {
|
||||
log('FAIL', `Pressure check stale: ${tokensSince} tokens ago (max: ${maxTokensAgo})`);
|
||||
log('INFO', 'Required: Run node scripts/check-session-pressure.js');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('PASS', `Pressure check recent: ${tokensSince} tokens ago`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkInstructionsLoaded() {
|
||||
const instructions = loadJSON(INSTRUCTION_HISTORY_PATH);
|
||||
|
||||
if (!instructions) {
|
||||
log('FAIL', 'Instruction history not loaded');
|
||||
log('INFO', 'Required: Ensure .claude/instruction-history.json exists and is loaded');
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeCount = instructions.instructions.filter(i => i.active).length;
|
||||
log('PASS', `Instruction database loaded: ${activeCount} active instructions`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkComponentForActionType(state, actionType) {
|
||||
const requirements = {
|
||||
'file-edit': ['CrossReferenceValidator'],
|
||||
'database': ['CrossReferenceValidator', 'BoundaryEnforcer'],
|
||||
'architecture': ['BoundaryEnforcer', 'MetacognitiveVerifier'],
|
||||
'config': ['CrossReferenceValidator'],
|
||||
'security': ['BoundaryEnforcer', 'MetacognitiveVerifier'],
|
||||
'values': ['BoundaryEnforcer'],
|
||||
'complex': ['MetacognitiveVerifier'],
|
||||
'document-deployment': ['BoundaryEnforcer', 'CrossReferenceValidator'], // NEW: Security check for doc deployment
|
||||
'general': []
|
||||
};
|
||||
|
||||
const required = requirements[actionType] || requirements['general'];
|
||||
const missing = [];
|
||||
|
||||
required.forEach(component => {
|
||||
const activity = state.last_framework_activity[component];
|
||||
const messagesSince = state.message_count - activity.message;
|
||||
|
||||
if (messagesSince > 10) {
|
||||
missing.push({ component, messagesSince });
|
||||
}
|
||||
});
|
||||
|
||||
if (missing.length > 0) {
|
||||
log('FAIL', `Required components not recently used for action type '${actionType}':`);
|
||||
missing.forEach(m => {
|
||||
log('FAIL', ` - ${m.component}: ${m.messagesSince} messages ago`);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (required.length > 0) {
|
||||
log('PASS', `Required components recently used: ${required.join(', ')}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkTokenCheckpoints() {
|
||||
const checkpoints = loadJSON(TOKEN_CHECKPOINTS_PATH);
|
||||
|
||||
if (!checkpoints) {
|
||||
log('WARN', 'Token checkpoints file not found');
|
||||
return true; // Non-blocking warning
|
||||
}
|
||||
|
||||
if (checkpoints.overdue) {
|
||||
log('FAIL', `Token checkpoint OVERDUE: ${checkpoints.next_checkpoint}`);
|
||||
log('INFO', 'Required: Run pressure check immediately');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('PASS', `Token checkpoints OK: next at ${checkpoints.next_checkpoint}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSP Compliance Checker
|
||||
* Validates HTML/JS files for Content Security Policy violations
|
||||
* (inst_008: "ALWAYS comply with CSP - no inline event handlers, no inline scripts")
|
||||
*/
|
||||
function checkCSPCompliance(filePath) {
|
||||
if (!filePath) {
|
||||
log('INFO', 'No file path provided - skipping CSP check');
|
||||
return true; // Non-blocking if no file specified
|
||||
}
|
||||
|
||||
// Only check HTML/JS files
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!['.html', '.js'].includes(ext)) {
|
||||
log('INFO', `File type ${ext} - skipping CSP check (only validates .html/.js)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve relative paths
|
||||
const absolutePath = path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: path.join(__dirname, '../', filePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
log('WARN', `File not found: ${absolutePath} - skipping CSP check`);
|
||||
return true; // Non-blocking warning
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(absolutePath, 'utf8');
|
||||
const violations = [];
|
||||
|
||||
// CSP Violation Patterns
|
||||
const patterns = [
|
||||
{
|
||||
name: 'Inline event handlers',
|
||||
regex: /\son\w+\s*=\s*["'][^"']*["']/gi,
|
||||
severity: 'CRITICAL',
|
||||
examples: ['onclick=', 'onload=', 'onerror=', 'onchange=']
|
||||
},
|
||||
{
|
||||
name: 'Inline styles',
|
||||
regex: /\sstyle\s*=\s*["'][^"']+["']/gi,
|
||||
severity: 'CRITICAL',
|
||||
examples: ['style="color: red"', 'style="line-height: 1"']
|
||||
},
|
||||
{
|
||||
name: 'Inline scripts (without src)',
|
||||
regex: /<script(?![^>]*\ssrc=)[^>]*>[\s\S]*?<\/script>/gi,
|
||||
severity: 'WARNING',
|
||||
examples: ['<script>alert("test")</script>'],
|
||||
// Allow empty or whitespace-only scripts (often used for templates)
|
||||
filter: (match) => match.replace(/<script[^>]*>|<\/script>/gi, '').trim().length > 0
|
||||
},
|
||||
{
|
||||
name: 'javascript: URLs',
|
||||
regex: /href\s*=\s*["']javascript:[^"']*["']/gi,
|
||||
severity: 'CRITICAL',
|
||||
examples: ['href="javascript:void(0)"']
|
||||
}
|
||||
];
|
||||
|
||||
patterns.forEach(pattern => {
|
||||
const matches = content.match(pattern.regex);
|
||||
if (matches) {
|
||||
const filtered = pattern.filter
|
||||
? matches.filter(pattern.filter)
|
||||
: matches;
|
||||
|
||||
if (filtered.length > 0) {
|
||||
violations.push({
|
||||
name: pattern.name,
|
||||
severity: pattern.severity,
|
||||
count: filtered.length,
|
||||
samples: filtered.slice(0, 3), // Show first 3 examples
|
||||
examples: pattern.examples
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (violations.length === 0) {
|
||||
log('PASS', `CSP compliance validated: ${path.basename(filePath)}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Report violations
|
||||
log('FAIL', `CSP violations detected in ${path.basename(filePath)}:`);
|
||||
violations.forEach(v => {
|
||||
log('FAIL', ` [${v.severity}] ${v.name} (${v.count} occurrences)`);
|
||||
v.samples.forEach((sample, idx) => {
|
||||
const truncated = sample.length > 80
|
||||
? sample.substring(0, 77) + '...'
|
||||
: sample;
|
||||
log('FAIL', ` ${idx + 1}. ${truncated}`);
|
||||
});
|
||||
});
|
||||
|
||||
log('INFO', '');
|
||||
log('INFO', 'CSP Violation Reference (inst_008):');
|
||||
log('INFO', ' - No inline event handlers (onclick=, onload=, etc.)');
|
||||
log('INFO', ' - No inline styles (style="" attribute)');
|
||||
log('INFO', ' - No inline scripts (<script> without src)');
|
||||
log('INFO', ' - No javascript: URLs');
|
||||
log('INFO', '');
|
||||
log('INFO', 'Fix: Move inline code to external .js/.css files');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Main validation
|
||||
function runPreActionCheck() {
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
log('INFO', `Validating action: ${actionType}`);
|
||||
log('INFO', `Description: ${actionDescription}`);
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
|
||||
// Activate contextual instructions BEFORE running checks
|
||||
activateContextualInstructions(filePath);
|
||||
|
||||
const state = loadJSON(SESSION_STATE_PATH);
|
||||
|
||||
if (!state) {
|
||||
log('ERROR', 'Session state not found. Framework may not be initialized.');
|
||||
log('ERROR', 'Run: node scripts/recover-framework.js');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const checks = [
|
||||
{ name: 'Pressure Check Recent', fn: () => checkPressureRecent(state) },
|
||||
{ name: 'Instructions Loaded', fn: () => checkInstructionsLoaded() },
|
||||
{ name: 'Token Checkpoints', fn: () => checkTokenCheckpoints() },
|
||||
{ name: 'CSP Compliance', fn: () => checkCSPCompliance(filePath) },
|
||||
{ name: 'Action-Specific Components', fn: () => checkComponentForActionType(state, actionType) }
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
|
||||
checks.forEach(check => {
|
||||
log('INFO', '');
|
||||
log('INFO', `Running check: ${check.name}`);
|
||||
const passed = check.fn();
|
||||
if (!passed) {
|
||||
allPassed = false;
|
||||
}
|
||||
});
|
||||
|
||||
log('INFO', '');
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
|
||||
if (allPassed) {
|
||||
// Track successful pre-action-check execution for hook enforcement
|
||||
trackPreActionCheck(state, filePath, actionType);
|
||||
|
||||
log('PASS', 'All checks passed. Action may proceed.');
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
process.exit(0);
|
||||
} else {
|
||||
log('FAIL', 'One or more checks failed. Action BLOCKED.');
|
||||
log('INFO', 'Required: Address failures above before proceeding.');
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate relevant instructions based on file context
|
||||
* Reminds about instructions that apply to this file type/path
|
||||
*/
|
||||
function activateContextualInstructions(filePath) {
|
||||
if (!filePath) {
|
||||
return; // No file specified, skip contextual activation
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const relevantInstructions = [];
|
||||
|
||||
// CSP compliance for HTML/JS files
|
||||
if (['.html', '.js'].includes(ext) && !filePath.includes('/scripts/')) {
|
||||
relevantInstructions.push({
|
||||
id: 'inst_008',
|
||||
description: 'CSP compliance - no inline styles, scripts, or event handlers',
|
||||
severity: 'CRITICAL'
|
||||
});
|
||||
}
|
||||
|
||||
// Internal documentation security
|
||||
if (filePath.includes('/admin/') || filePath.includes('internal') || filePath.includes('confidential')) {
|
||||
relevantInstructions.push({
|
||||
id: 'inst_012',
|
||||
description: 'NEVER deploy internal/confidential documents to public',
|
||||
severity: 'CRITICAL'
|
||||
});
|
||||
}
|
||||
|
||||
// API endpoint security
|
||||
if (filePath.includes('/api/') || filePath.includes('controller') || filePath.includes('route')) {
|
||||
relevantInstructions.push({
|
||||
id: 'inst_013',
|
||||
description: 'NEVER expose sensitive runtime data in public endpoints',
|
||||
severity: 'CRITICAL'
|
||||
});
|
||||
relevantInstructions.push({
|
||||
id: 'inst_045',
|
||||
description: 'API rate limiting, authentication, input validation required',
|
||||
severity: 'HIGH'
|
||||
});
|
||||
}
|
||||
|
||||
// Security middleware
|
||||
if (filePath.includes('middleware') && filePath.includes('security')) {
|
||||
relevantInstructions.push({
|
||||
id: 'inst_041',
|
||||
description: 'File uploads require malware scanning',
|
||||
severity: 'CRITICAL'
|
||||
});
|
||||
relevantInstructions.push({
|
||||
id: 'inst_043',
|
||||
description: 'User input requires sanitization and validation',
|
||||
severity: 'CRITICAL'
|
||||
});
|
||||
}
|
||||
|
||||
// Values content
|
||||
if (filePath.includes('values') || filePath.includes('ethics') || filePath.includes('privacy-policy')) {
|
||||
relevantInstructions.push({
|
||||
id: 'inst_005',
|
||||
description: 'Human approval required for values-sensitive content',
|
||||
severity: 'CRITICAL'
|
||||
});
|
||||
}
|
||||
|
||||
// Display relevant instructions
|
||||
if (relevantInstructions.length > 0) {
|
||||
log('INFO', '');
|
||||
log('INFO', '📋 ACTIVE INSTRUCTIONS FOR THIS FILE:');
|
||||
relevantInstructions.forEach(inst => {
|
||||
const indicator = inst.severity === 'CRITICAL' ? '🔴' : '🟡';
|
||||
log('INFO', ` ${indicator} ${inst.id}: ${inst.description}`);
|
||||
});
|
||||
log('INFO', '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track pre-action-check execution in session state
|
||||
* Hooks will verify this was run before allowing file modifications
|
||||
*/
|
||||
function trackPreActionCheck(state, filePath, actionType) {
|
||||
try {
|
||||
if (!state.last_framework_activity) {
|
||||
state.last_framework_activity = {};
|
||||
}
|
||||
|
||||
state.last_framework_activity.PreActionCheck = {
|
||||
timestamp: new Date().toISOString(),
|
||||
file: filePath,
|
||||
actionType: actionType,
|
||||
message: state.message_count,
|
||||
tokens: state.token_estimate
|
||||
};
|
||||
|
||||
state.last_updated = new Date().toISOString();
|
||||
|
||||
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(state, null, 2));
|
||||
} catch (error) {
|
||||
// Non-critical - don't fail pre-action-check if tracking fails
|
||||
log('WARN', `Could not track pre-action-check: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the check
|
||||
runPreActionCheck();
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
const { MongoClient } = require('mongodb');
|
||||
|
||||
async function queryAllDocuments() {
|
||||
const client = new MongoClient('mongodb://localhost:27017');
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db('tractatus_dev');
|
||||
const collection = db.collection('documents');
|
||||
|
||||
const documents = await collection.find({
|
||||
visibility: 'public'
|
||||
})
|
||||
.sort({ category: 1, order: 1, 'metadata.date_created': -1 })
|
||||
.toArray();
|
||||
|
||||
console.log(`\n=== TOTAL DOCUMENTS: ${documents.length} ===\n`);
|
||||
|
||||
const byCategory = {};
|
||||
documents.forEach(doc => {
|
||||
const cat = doc.category || 'none';
|
||||
if (!byCategory[cat]) {
|
||||
byCategory[cat] = [];
|
||||
}
|
||||
byCategory[cat].push({
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
order: doc.order || 999,
|
||||
category: doc.category || 'none',
|
||||
quadrant: doc.quadrant || 'N/A',
|
||||
audience: doc.audience || 'general'
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(byCategory).sort().forEach(category => {
|
||||
console.log(`\n━━━ CATEGORY: ${category} (${byCategory[category].length} docs) ━━━`);
|
||||
byCategory[category].forEach((doc, i) => {
|
||||
console.log(`${i + 1}. [order:${doc.order}] ${doc.title}`);
|
||||
console.log(` slug: ${doc.slug}`);
|
||||
console.log(` quadrant: ${doc.quadrant} | audience: ${doc.audience}`);
|
||||
});
|
||||
});
|
||||
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
queryAllDocuments().catch(console.error);
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
|
||||
const slugs = [
|
||||
'case-studies-real-world-llm-failure-modes-appendix',
|
||||
'research-topic-concurrent-session-architecture',
|
||||
'research-topic-rule-proliferation-transactional-overhead',
|
||||
'implementation-guide-python-examples',
|
||||
'tractatus-framework-enforcement-claude-code'
|
||||
];
|
||||
|
||||
async function main() {
|
||||
await connect();
|
||||
|
||||
console.log('Querying for 5 archives documents:\n');
|
||||
|
||||
for (const slug of slugs) {
|
||||
const doc = await Document.findBySlug(slug);
|
||||
if (doc) {
|
||||
const sections = doc.sections ? doc.sections.length : 0;
|
||||
console.log(`✅ ${slug}`);
|
||||
console.log(` category: ${doc.category}, order: ${doc.order}, visibility: ${doc.visibility || 'public'}, sections: ${sections}`);
|
||||
} else {
|
||||
console.log(`❌ ${slug} - NOT FOUND`);
|
||||
}
|
||||
}
|
||||
|
||||
await close();
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Recategorize Architectural Safeguards Document Sections
|
||||
* Applies logical categorization based on content purpose
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
|
||||
// Manual section categorization mapping
|
||||
const SECTION_CATEGORIES = {
|
||||
// Conceptual Foundation - Understanding the problem and solution
|
||||
'How Tractatus Protects Plural Morals from AI Pattern Bias': 'conceptual',
|
||||
'Executive Summary': 'conceptual',
|
||||
'3. The Dichotomy Resolved: Hierarchical Boundaries + Non-Hierarchical Deliberation': 'conceptual',
|
||||
'6. The Ultimate Safeguard: User Can Fork the System': 'conceptual',
|
||||
'7. Summary: How Tractatus Prevents Runaway AI': 'conceptual',
|
||||
'9. Conclusion: The Fight Against Amoral Intelligence': 'conceptual',
|
||||
|
||||
// Technical Implementation - How the system works architecturally
|
||||
'1. The Structural Architecture: Three Layers of Protection': 'technical',
|
||||
'5. Extending to Multi-User Contexts: Preventing Majority Dominance': 'technical',
|
||||
|
||||
// Practical Application - Using and testing the system
|
||||
'2. How This Prevents LLM Hierarchical Dominance': 'practical',
|
||||
'4. What Happens If LLM Tries to Dominate Anyway?': 'practical',
|
||||
'Appendix B: Red-Team Scenarios (Adversarial Testing)': 'practical',
|
||||
'Appendix C: Implementation Checklist': 'practical',
|
||||
|
||||
// Reference Material - Metadata, comparisons, research questions
|
||||
'8. Open Questions & Future Research': 'reference',
|
||||
'Contact': 'reference',
|
||||
'License': 'reference',
|
||||
'Document Metadata': 'reference',
|
||||
'Appendix A: Comparison to Other AI Governance Approaches': 'reference'
|
||||
};
|
||||
|
||||
async function recategorizeSections() {
|
||||
try {
|
||||
console.log('\n=== Recategorizing Architectural Safeguards Sections ===\n');
|
||||
|
||||
const slug = 'architectural-safeguards-against-llm-hierarchical-dominance-prose';
|
||||
|
||||
// Connect to database
|
||||
await connect();
|
||||
|
||||
// Find document
|
||||
console.log('📊 Finding document in database...');
|
||||
const doc = await Document.findBySlug(slug);
|
||||
|
||||
if (!doc) {
|
||||
console.error('❌ Error: Document not found');
|
||||
await close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✓ Found document with', doc.sections.length, 'sections');
|
||||
|
||||
// Update section categories
|
||||
console.log('\n🔄 Applying logical categorization...\n');
|
||||
|
||||
let updated = 0;
|
||||
doc.sections.forEach(section => {
|
||||
const newCategory = SECTION_CATEGORIES[section.title];
|
||||
if (newCategory) {
|
||||
const oldCategory = section.category;
|
||||
section.category = newCategory;
|
||||
if (oldCategory !== newCategory) {
|
||||
console.log(` "${section.title}"`);
|
||||
console.log(` ${oldCategory} → ${newCategory}`);
|
||||
updated++;
|
||||
}
|
||||
} else {
|
||||
console.log(` ⚠️ No mapping for: "${section.title}" (keeping ${section.category})`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n✓ Updated ${updated} section categories`);
|
||||
|
||||
// Save updated document
|
||||
console.log('\n💾 Saving changes...');
|
||||
const result = await Document.update(doc._id, {
|
||||
sections: doc.sections
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
console.error('❌ Error: Failed to update document');
|
||||
await close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✓ Changes saved successfully!');
|
||||
|
||||
// Show final breakdown
|
||||
console.log('\n📋 Final Section Breakdown:');
|
||||
const categoryCounts = {};
|
||||
doc.sections.forEach(section => {
|
||||
categoryCounts[section.category] = (categoryCounts[section.category] || 0) + 1;
|
||||
});
|
||||
|
||||
const categoryLabels = {
|
||||
conceptual: '📘 Conceptual',
|
||||
technical: '🔧 Technical',
|
||||
practical: '✨ Practical',
|
||||
reference: '📋 Reference'
|
||||
};
|
||||
|
||||
Object.entries(categoryCounts).sort().forEach(([category, count]) => {
|
||||
const label = categoryLabels[category] || category;
|
||||
console.log(` ${label}: ${count} sections`);
|
||||
});
|
||||
|
||||
console.log('\n✅ Recategorization complete!');
|
||||
console.log(` View at: https://agenticgovernance.digital/docs.html?doc=${slug}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Recategorization failed:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
recategorizeSections();
|
||||
}
|
||||
|
||||
module.exports = recategorizeSections;
|
||||
|
|
@ -1,321 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Framework Recovery Protocol - Fail-Safe Recovery
|
||||
*
|
||||
* This script is run when framework fade is detected (components not being used).
|
||||
* It performs diagnostics, reports findings, and helps re-establish baseline.
|
||||
*
|
||||
* CRITICAL: This is a Claude Code-specific enforcement mechanism.
|
||||
*
|
||||
* Recovery Steps:
|
||||
* 1. Diagnose current state
|
||||
* 2. Report all issues found
|
||||
* 3. Clear stale alerts
|
||||
* 4. Reset monitoring thresholds
|
||||
* 5. Recommend actions for Claude
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const SESSION_STATE_PATH = path.join(__dirname, '../.claude/session-state.json');
|
||||
const TOKEN_CHECKPOINTS_PATH = path.join(__dirname, '../.claude/token-checkpoints.json');
|
||||
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
green: '\x1b[32m',
|
||||
cyan: '\x1b[36m',
|
||||
magenta: '\x1b[35m',
|
||||
bold: '\x1b[1m'
|
||||
};
|
||||
|
||||
function log(level, message) {
|
||||
const prefix = {
|
||||
INFO: `${colors.cyan}[RECOVERY]${colors.reset}`,
|
||||
SUCCESS: `${colors.green}${colors.bold}[RECOVERY SUCCESS]${colors.reset}`,
|
||||
ERROR: `${colors.red}${colors.bold}[RECOVERY ERROR]${colors.reset}`,
|
||||
WARN: `${colors.yellow}[RECOVERY WARN]${colors.reset}`,
|
||||
ACTION: `${colors.magenta}${colors.bold}[ACTION REQUIRED]${colors.reset}`
|
||||
}[level] || '[RECOVERY]';
|
||||
|
||||
console.log(`${prefix} ${message}`);
|
||||
}
|
||||
|
||||
function loadJSON(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch (error) {
|
||||
log('ERROR', `Failed to load ${filePath}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveJSON(filePath, data) {
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
return true;
|
||||
} catch (error) {
|
||||
log('ERROR', `Failed to save ${filePath}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function diagnoseFrameworkState() {
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
log('INFO', 'TRACTATUS FRAMEWORK RECOVERY INITIATED');
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
log('INFO', '');
|
||||
|
||||
const issues = [];
|
||||
const recommendations = [];
|
||||
|
||||
// Check session state
|
||||
log('INFO', 'Step 1: Checking session state...');
|
||||
const state = loadJSON(SESSION_STATE_PATH);
|
||||
|
||||
if (!state) {
|
||||
issues.push({
|
||||
severity: 'CRITICAL',
|
||||
component: 'System',
|
||||
issue: 'Session state file not found or corrupted',
|
||||
action: 'Re-initialize session-state.json with baseline values'
|
||||
});
|
||||
} else {
|
||||
log('SUCCESS', `Session state loaded: ${state.message_count} messages, ~${state.token_estimate} tokens`);
|
||||
|
||||
// Check each component
|
||||
const components = [
|
||||
'ContextPressureMonitor',
|
||||
'InstructionPersistenceClassifier',
|
||||
'CrossReferenceValidator',
|
||||
'BoundaryEnforcer',
|
||||
'MetacognitiveVerifier'
|
||||
];
|
||||
|
||||
components.forEach(component => {
|
||||
const activity = state.last_framework_activity[component];
|
||||
const messagesSince = state.message_count - activity.message;
|
||||
const tokensSince = state.token_estimate - activity.tokens;
|
||||
|
||||
if (activity.message === 0) {
|
||||
issues.push({
|
||||
severity: 'HIGH',
|
||||
component,
|
||||
issue: 'Never used in this session',
|
||||
action: `Immediately invoke ${component}`
|
||||
});
|
||||
} else if (messagesSince > state.staleness_thresholds.messages) {
|
||||
issues.push({
|
||||
severity: 'MEDIUM',
|
||||
component,
|
||||
issue: `Stale: ${messagesSince} messages ago (threshold: ${state.staleness_thresholds.messages})`,
|
||||
action: `Re-invoke ${component} if appropriate for current task`
|
||||
});
|
||||
} else {
|
||||
log('SUCCESS', `${component}: Active (${messagesSince} messages ago)`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for active alerts
|
||||
if (state.alerts && state.alerts.length > 0) {
|
||||
log('WARN', `${state.alerts.length} active alerts in session state`);
|
||||
state.alerts.forEach(alert => {
|
||||
issues.push({
|
||||
severity: alert.severity,
|
||||
component: alert.component,
|
||||
issue: alert.message,
|
||||
action: 'Address underlying issue'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log('INFO', '');
|
||||
|
||||
// Check token checkpoints
|
||||
log('INFO', 'Step 2: Checking token checkpoints...');
|
||||
const checkpoints = loadJSON(TOKEN_CHECKPOINTS_PATH);
|
||||
|
||||
if (!checkpoints) {
|
||||
issues.push({
|
||||
severity: 'HIGH',
|
||||
component: 'ContextPressureMonitor',
|
||||
issue: 'Token checkpoints file not found',
|
||||
action: 'Re-initialize token-checkpoints.json'
|
||||
});
|
||||
} else {
|
||||
if (checkpoints.overdue) {
|
||||
issues.push({
|
||||
severity: 'CRITICAL',
|
||||
component: 'ContextPressureMonitor',
|
||||
issue: `Checkpoint OVERDUE: ${checkpoints.next_checkpoint} (current: ~${state?.token_estimate || 'unknown'})`,
|
||||
action: 'Run pressure check immediately: node scripts/check-session-pressure.js'
|
||||
});
|
||||
} else {
|
||||
log('SUCCESS', `Next checkpoint: ${checkpoints.next_checkpoint}`);
|
||||
}
|
||||
}
|
||||
|
||||
log('INFO', '');
|
||||
|
||||
// Check instruction history
|
||||
log('INFO', 'Step 3: Checking instruction database...');
|
||||
const instructions = loadJSON(INSTRUCTION_HISTORY_PATH);
|
||||
|
||||
if (!instructions) {
|
||||
issues.push({
|
||||
severity: 'MEDIUM',
|
||||
component: 'InstructionPersistenceClassifier',
|
||||
issue: 'Instruction history not found',
|
||||
action: 'Ensure .claude/instruction-history.json exists'
|
||||
});
|
||||
} else {
|
||||
const activeCount = instructions.instructions.filter(i => i.active).length;
|
||||
log('SUCCESS', `Instruction database loaded: ${activeCount} active instructions`);
|
||||
}
|
||||
|
||||
log('INFO', '');
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
|
||||
return { issues, state, checkpoints, instructions };
|
||||
}
|
||||
|
||||
function reportIssues(issues) {
|
||||
if (issues.length === 0) {
|
||||
log('SUCCESS', 'No issues found. Framework is operational.');
|
||||
return;
|
||||
}
|
||||
|
||||
log('ERROR', `FOUND ${issues.length} ISSUES:`);
|
||||
log('ERROR', '');
|
||||
|
||||
const critical = issues.filter(i => i.severity === 'CRITICAL');
|
||||
const high = issues.filter(i => i.severity === 'HIGH');
|
||||
const medium = issues.filter(i => i.severity === 'MEDIUM');
|
||||
|
||||
if (critical.length > 0) {
|
||||
log('ERROR', `CRITICAL ISSUES (${critical.length}):`);
|
||||
critical.forEach((issue, idx) => {
|
||||
log('ERROR', `${idx + 1}. [${issue.component}] ${issue.issue}`);
|
||||
log('ACTION', ` → ${issue.action}`);
|
||||
});
|
||||
log('ERROR', '');
|
||||
}
|
||||
|
||||
if (high.length > 0) {
|
||||
log('WARN', `HIGH PRIORITY ISSUES (${high.length}):`);
|
||||
high.forEach((issue, idx) => {
|
||||
log('WARN', `${idx + 1}. [${issue.component}] ${issue.issue}`);
|
||||
log('ACTION', ` → ${issue.action}`);
|
||||
});
|
||||
log('WARN', '');
|
||||
}
|
||||
|
||||
if (medium.length > 0) {
|
||||
log('INFO', `MEDIUM PRIORITY ISSUES (${medium.length}):`);
|
||||
medium.forEach((issue, idx) => {
|
||||
log('INFO', `${idx + 1}. [${issue.component}] ${issue.issue}`);
|
||||
log('ACTION', ` → ${issue.action}`);
|
||||
});
|
||||
log('INFO', '');
|
||||
}
|
||||
}
|
||||
|
||||
function performRecovery(state, checkpoints) {
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
log('INFO', 'Step 4: Performing recovery actions...');
|
||||
log('INFO', '');
|
||||
|
||||
let recovered = true;
|
||||
|
||||
// Clear alerts from session state
|
||||
if (state && state.alerts && state.alerts.length > 0) {
|
||||
log('INFO', 'Clearing stale alerts from session state...');
|
||||
state.alerts = [];
|
||||
state.last_updated = new Date().toISOString();
|
||||
if (saveJSON(SESSION_STATE_PATH, state)) {
|
||||
log('SUCCESS', 'Session state alerts cleared');
|
||||
} else {
|
||||
recovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset checkpoint overdue flag if needed
|
||||
if (checkpoints && checkpoints.overdue) {
|
||||
log('WARN', 'Checkpoint overdue flag is set - will remain until pressure check runs');
|
||||
}
|
||||
|
||||
log('INFO', '');
|
||||
log('INFO', '═══════════════════════════════════════════════════════════');
|
||||
|
||||
return recovered;
|
||||
}
|
||||
|
||||
function provideRecommendations(issues) {
|
||||
log('ACTION', 'IMMEDIATE ACTIONS FOR CLAUDE:');
|
||||
log('ACTION', '');
|
||||
|
||||
const critical = issues.filter(i => i.severity === 'CRITICAL');
|
||||
const hasStaleComponents = issues.some(i => i.component !== 'System' && i.component !== 'ContextPressureMonitor');
|
||||
|
||||
if (critical.length > 0) {
|
||||
log('ACTION', '1. STOP all current work immediately');
|
||||
log('ACTION', '2. Address all CRITICAL issues listed above');
|
||||
log('ACTION', '3. Run pressure check if overdue');
|
||||
} else if (hasStaleComponents) {
|
||||
log('ACTION', '1. Review which components are stale');
|
||||
log('ACTION', '2. Consider if they should be invoked for recent actions');
|
||||
log('ACTION', '3. Increase monitoring frequency');
|
||||
} else {
|
||||
log('ACTION', '1. Resume work with normal monitoring');
|
||||
log('ACTION', '2. Be vigilant about using all five components');
|
||||
}
|
||||
|
||||
log('ACTION', '');
|
||||
log('ACTION', 'ONGOING REQUIREMENTS:');
|
||||
log('ACTION', '- Use ContextPressureMonitor every 25% tokens (50k)');
|
||||
log('ACTION', '- Use InstructionPersistenceClassifier for explicit directives');
|
||||
log('ACTION', '- Use CrossReferenceValidator before major changes');
|
||||
log('ACTION', '- Use BoundaryEnforcer before values decisions');
|
||||
log('ACTION', '- Use MetacognitiveVerifier for complex operations (>3 files)');
|
||||
log('ACTION', '');
|
||||
log('ACTION', '═══════════════════════════════════════════════════════════');
|
||||
}
|
||||
|
||||
// Main recovery process
|
||||
function runRecovery() {
|
||||
const { issues, state, checkpoints, instructions } = diagnoseFrameworkState();
|
||||
|
||||
reportIssues(issues);
|
||||
|
||||
const recovered = performRecovery(state, checkpoints);
|
||||
|
||||
provideRecommendations(issues);
|
||||
|
||||
log('INFO', '');
|
||||
if (recovered && issues.length === 0) {
|
||||
log('SUCCESS', 'Framework recovery COMPLETE. All systems operational.');
|
||||
process.exit(0);
|
||||
} else if (recovered) {
|
||||
log('WARN', 'Framework recovery PARTIAL. Issues require attention.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
log('ERROR', 'Framework recovery FAILED. Manual intervention required.');
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Run recovery
|
||||
runRecovery();
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Remove Duplicate/Old Documents
|
||||
*
|
||||
* Removes 5 duplicate documents that were replaced by newer versions
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
|
||||
const DOCUMENTS_TO_REMOVE = [
|
||||
{ slug: 'case-studies', reason: 'Duplicate - replaced by case-studies-real-world-llm-failure-modes-appendix' },
|
||||
{ slug: 'concurrent-session-architecture-limitations', reason: 'Duplicate - replaced by research-topic-concurrent-session-architecture' },
|
||||
{ slug: 'rule-proliferation-and-transactional-overhead', reason: 'Duplicate - replaced by research-topic-rule-proliferation-transactional-overhead' },
|
||||
{ slug: 'implementation-guide-python-code-examples', reason: 'Old version without sections - replaced by implementation-guide-python-examples' },
|
||||
{ slug: 'framework-governance-in-action-pre-publication-security-audit', reason: 'Incomplete document without sections' }
|
||||
];
|
||||
|
||||
async function removeDocument(slug, reason) {
|
||||
try {
|
||||
const doc = await Document.findBySlug(slug);
|
||||
|
||||
if (!doc) {
|
||||
console.log(` ⏭️ Not found: ${slug}`);
|
||||
return { success: false, reason: 'not_found' };
|
||||
}
|
||||
|
||||
console.log(`\n❌ Removing: ${doc.title}`);
|
||||
console.log(` Slug: ${slug}`);
|
||||
console.log(` Reason: ${reason}`);
|
||||
|
||||
const deleted = await Document.delete(doc._id.toString());
|
||||
|
||||
if (!deleted) {
|
||||
console.log(` ❌ Failed to delete`);
|
||||
return { success: false, reason: 'delete_failed' };
|
||||
}
|
||||
|
||||
console.log(` ✅ Deleted successfully`);
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🗑️ Removing Duplicate/Old Documents\n');
|
||||
console.log('═══════════════════════════════════════════════════\n');
|
||||
|
||||
await connect();
|
||||
|
||||
let removed = 0;
|
||||
let notFound = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const { slug, reason } of DOCUMENTS_TO_REMOVE) {
|
||||
const result = await removeDocument(slug, reason);
|
||||
|
||||
if (result.success) {
|
||||
removed++;
|
||||
} else if (result.reason === 'not_found') {
|
||||
notFound++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════');
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` ✅ Removed: ${removed}`);
|
||||
console.log(` ⏭️ Not found: ${notFound}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📦 Total: ${DOCUMENTS_TO_REMOVE.length}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Reorganize docs.html sidebar - Update document metadata for better UX
|
||||
*
|
||||
* New structure based on audience and expertise level:
|
||||
* - Introduction (1-5): Absolute beginners, all audiences
|
||||
* - Implementation (10-19): Practical/technical for implementers
|
||||
* - Case Studies (20-29): Real-world examples, all audiences
|
||||
* - Business Strategy (30-35): Leaders/decision makers
|
||||
* - Advanced Topics (40-49): Deep technical for experts
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
require('dotenv').config();
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
|
||||
// Document reorganization mapping
|
||||
// Format: { slug: { order, category, audience, description } }
|
||||
const DOCUMENT_MAPPING = {
|
||||
// INTRODUCTION (1-5) - Absolute beginners, all audiences
|
||||
'architectural-overview-and-research-status': {
|
||||
order: 1,
|
||||
category: 'introduction',
|
||||
audience: 'general',
|
||||
description: 'Start here - overview of the Tractatus Framework'
|
||||
},
|
||||
'core-concepts-of-the-tractatus-framework': {
|
||||
order: 2,
|
||||
category: 'introduction',
|
||||
audience: 'general',
|
||||
description: 'Core concepts and principles'
|
||||
},
|
||||
'tractatus-ai-safety-framework-core-values-and-principles': {
|
||||
order: 3,
|
||||
category: 'introduction',
|
||||
audience: 'general',
|
||||
description: 'Values and principles'
|
||||
},
|
||||
'technical-architecture': {
|
||||
order: 4,
|
||||
category: 'introduction',
|
||||
audience: 'technical',
|
||||
description: 'System architecture overview'
|
||||
},
|
||||
'tractatus-agentic-governance-system-glossary-of-terms': {
|
||||
order: 5,
|
||||
category: 'introduction',
|
||||
audience: 'general',
|
||||
description: 'Glossary and terminology'
|
||||
},
|
||||
|
||||
// IMPLEMENTATION (10-19) - Practical/technical for implementers
|
||||
'implementation-guide': {
|
||||
order: 10,
|
||||
category: 'implementation',
|
||||
audience: 'implementer',
|
||||
description: 'Complete implementation guide'
|
||||
},
|
||||
'tractatus-framework-implementation-guide': {
|
||||
order: 11,
|
||||
category: 'implementation',
|
||||
audience: 'implementer',
|
||||
description: 'Implementation guide v1.1 (detailed)'
|
||||
},
|
||||
'comparison-matrix-claude-code-claudemd-and-tractatus-framework': {
|
||||
order: 12,
|
||||
category: 'implementation',
|
||||
audience: 'implementer',
|
||||
description: 'Comparing different governance approaches'
|
||||
},
|
||||
|
||||
// CASE STUDIES (20-29) - Real-world examples, all audiences
|
||||
'the-27027-incident-a-case-study-in-pattern-recognition-bias': {
|
||||
order: 20,
|
||||
category: 'case-studies',
|
||||
audience: 'general',
|
||||
description: 'The famous 27027 incident - pattern recognition bias'
|
||||
},
|
||||
'our-framework-in-action-detecting-and-correcting-ai-fabrications': {
|
||||
order: 21,
|
||||
category: 'case-studies',
|
||||
audience: 'general',
|
||||
description: 'How Tractatus catches AI fabrications'
|
||||
},
|
||||
'when-frameworks-fail-and-why-thats-ok': {
|
||||
order: 22,
|
||||
category: 'case-studies',
|
||||
audience: 'general',
|
||||
description: 'Learning from framework failures'
|
||||
},
|
||||
'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery': {
|
||||
order: 23,
|
||||
category: 'case-studies',
|
||||
audience: 'general',
|
||||
description: 'Failure analysis and recovery process'
|
||||
},
|
||||
'framework-governance-in-action-pre-publication-security-audit': {
|
||||
order: 24,
|
||||
category: 'case-studies',
|
||||
audience: 'technical',
|
||||
description: 'Security audit before publication'
|
||||
},
|
||||
'case-studies-real-world-llm-failure-modes': {
|
||||
order: 25,
|
||||
category: 'case-studies',
|
||||
audience: 'researcher',
|
||||
description: 'Collection of real-world LLM failures'
|
||||
},
|
||||
|
||||
// BUSINESS STRATEGY (30-35) - Leaders/decision makers
|
||||
'ai-governance-business-case-template-tractatus-framework': {
|
||||
order: 30,
|
||||
category: 'business',
|
||||
audience: 'leader',
|
||||
description: 'Business case template for AI governance'
|
||||
}
|
||||
};
|
||||
|
||||
async function reorganizeDocuments() {
|
||||
console.log('╔════════════════════════════════════════════════════════════╗');
|
||||
console.log('║ Reorganizing Documentation Sidebar Structure ║');
|
||||
console.log('╚════════════════════════════════════════════════════════════╝\n');
|
||||
|
||||
const client = new MongoClient(MONGODB_URI);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('✓ Connected to MongoDB\n');
|
||||
|
||||
const db = client.db();
|
||||
const documentsCollection = db.collection('documents');
|
||||
|
||||
// Get all documents
|
||||
const allDocs = await documentsCollection.find({}).toArray();
|
||||
console.log(`Found ${allDocs.length} total documents\n`);
|
||||
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
// Update each mapped document
|
||||
for (const [slug, metadata] of Object.entries(DOCUMENT_MAPPING)) {
|
||||
const doc = allDocs.find(d => d.slug === slug);
|
||||
|
||||
if (!doc) {
|
||||
console.log(`⚠ Document not found: ${slug}`);
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const updateFields = {
|
||||
order: metadata.order,
|
||||
category: metadata.category,
|
||||
audience: metadata.audience,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
await documentsCollection.updateOne(
|
||||
{ slug },
|
||||
{ $set: updateFields }
|
||||
);
|
||||
|
||||
console.log(`✓ Updated: ${doc.title}`);
|
||||
console.log(` Order: ${metadata.order} | Category: ${metadata.category} | Audience: ${metadata.audience}\n`);
|
||||
updated++;
|
||||
}
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════════\n');
|
||||
console.log(`✓ Updated: ${updated} documents`);
|
||||
console.log(`⊘ Skipped: ${skipped} documents`);
|
||||
console.log(`\n═══════════════════════════════════════════════════════════\n`);
|
||||
|
||||
// Show new category distribution
|
||||
console.log('New Category Distribution:\n');
|
||||
|
||||
const categories = {
|
||||
introduction: { count: 0, orders: '1-5' },
|
||||
implementation: { count: 0, orders: '10-19' },
|
||||
'case-studies': { count: 0, orders: '20-29' },
|
||||
business: { count: 0, orders: '30-35' },
|
||||
advanced: { count: 0, orders: '40-49' }
|
||||
};
|
||||
|
||||
Object.values(DOCUMENT_MAPPING).forEach(meta => {
|
||||
if (categories[meta.category]) {
|
||||
categories[meta.category].count++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` 📘 Introduction (${categories.introduction.count} docs) - Orders ${categories.introduction.orders}`);
|
||||
console.log(` ⚙️ Implementation (${categories.implementation.count} docs) - Orders ${categories.implementation.orders}`);
|
||||
console.log(` 📊 Case Studies (${categories['case-studies'].count} docs) - Orders ${categories['case-studies'].orders}`);
|
||||
console.log(` 💼 Business Strategy (${categories.business.count} docs) - Orders ${categories.business.orders}`);
|
||||
console.log(` 🔬 Advanced Topics (${categories.advanced.count} docs) - Orders ${categories.advanced.orders}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
console.log('\n✓ Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Run
|
||||
reorganizeDocuments().catch(console.error);
|
||||
|
|
@ -1,476 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Security Audit Script
|
||||
* Checks for common security vulnerabilities and best practices
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// ANSI colors for output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
const issues = {
|
||||
critical: [],
|
||||
high: [],
|
||||
medium: [],
|
||||
low: [],
|
||||
info: []
|
||||
};
|
||||
|
||||
function log(level, message, detail = '') {
|
||||
const levelColors = {
|
||||
CRITICAL: colors.red,
|
||||
HIGH: colors.red,
|
||||
MEDIUM: colors.yellow,
|
||||
LOW: colors.cyan,
|
||||
INFO: colors.blue,
|
||||
PASS: colors.green
|
||||
};
|
||||
|
||||
const color = levelColors[level] || colors.reset;
|
||||
console.log(`${color}[${level}]${colors.reset} ${message}`);
|
||||
if (detail) {
|
||||
console.log(` ${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
function addIssue(severity, title, description, remediation) {
|
||||
const issue = { title, description, remediation };
|
||||
issues[severity].push(issue);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 1. CHECK ENVIRONMENT VARIABLES
|
||||
// ============================================================================
|
||||
function checkEnvironmentVariables() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '1. Environment Variables Security' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
const requiredSecrets = [
|
||||
'JWT_SECRET',
|
||||
'SESSION_SECRET',
|
||||
'MONGODB_URI'
|
||||
];
|
||||
|
||||
const envExamplePath = path.join(__dirname, '../.env.example');
|
||||
const envPath = path.join(__dirname, '../.env');
|
||||
|
||||
// Check if .env.example exists
|
||||
if (!fs.existsSync(envExamplePath)) {
|
||||
addIssue('medium', 'Missing .env.example',
|
||||
'.env.example file not found',
|
||||
'Create .env.example with placeholder values for all required environment variables');
|
||||
log('MEDIUM', 'Missing .env.example file');
|
||||
} else {
|
||||
log('PASS', '.env.example file exists');
|
||||
}
|
||||
|
||||
// Check if .env is in .gitignore
|
||||
const gitignorePath = path.join(__dirname, '../.gitignore');
|
||||
if (fs.existsSync(gitignorePath)) {
|
||||
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
||||
if (!gitignoreContent.includes('.env')) {
|
||||
addIssue('critical', '.env not in .gitignore',
|
||||
'.env file might be committed to git',
|
||||
'Add .env to .gitignore immediately');
|
||||
log('CRITICAL', '.env not found in .gitignore');
|
||||
} else {
|
||||
log('PASS', '.env is in .gitignore');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hardcoded secrets in code
|
||||
const srcDir = path.join(__dirname, '../src');
|
||||
try {
|
||||
const grepCmd = `grep -r -i "password\\s*=\\s*['\"]\\|secret\\s*=\\s*['\"]\\|api[_-]key\\s*=\\s*['\"]" ${srcDir} || true`;
|
||||
const result = execSync(grepCmd, { encoding: 'utf8' });
|
||||
if (result.trim()) {
|
||||
addIssue('critical', 'Hardcoded secrets detected',
|
||||
`Found potential hardcoded secrets:\n${result}`,
|
||||
'Remove hardcoded secrets and use environment variables');
|
||||
log('CRITICAL', 'Potential hardcoded secrets found');
|
||||
console.log(result);
|
||||
} else {
|
||||
log('PASS', 'No hardcoded secrets detected in src/');
|
||||
}
|
||||
} catch (err) {
|
||||
// grep returns non-zero if no matches, which is good
|
||||
log('PASS', 'No hardcoded secrets detected in src/');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 2. CHECK DEPENDENCIES FOR VULNERABILITIES
|
||||
// ============================================================================
|
||||
function checkDependencies() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '2. Dependency Vulnerabilities' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
try {
|
||||
log('INFO', 'Running npm audit...');
|
||||
const auditResult = execSync('npm audit --json', { encoding: 'utf8' });
|
||||
const audit = JSON.parse(auditResult);
|
||||
|
||||
if (audit.vulnerabilities) {
|
||||
const vulns = audit.vulnerabilities;
|
||||
const critical = Object.values(vulns).filter(v => v.severity === 'critical').length;
|
||||
const high = Object.values(vulns).filter(v => v.severity === 'high').length;
|
||||
const moderate = Object.values(vulns).filter(v => v.severity === 'moderate').length;
|
||||
const low = Object.values(vulns).filter(v => v.severity === 'low').length;
|
||||
|
||||
if (critical > 0) {
|
||||
addIssue('critical', 'Critical dependency vulnerabilities',
|
||||
`Found ${critical} critical vulnerabilities`,
|
||||
'Run npm audit fix or update vulnerable dependencies');
|
||||
log('CRITICAL', `${critical} critical vulnerabilities`);
|
||||
}
|
||||
if (high > 0) {
|
||||
addIssue('high', 'High severity dependency vulnerabilities',
|
||||
`Found ${high} high severity vulnerabilities`,
|
||||
'Run npm audit fix or update vulnerable dependencies');
|
||||
log('HIGH', `${high} high severity vulnerabilities`);
|
||||
}
|
||||
if (moderate > 0) {
|
||||
log('MEDIUM', `${moderate} moderate severity vulnerabilities`);
|
||||
}
|
||||
if (low > 0) {
|
||||
log('LOW', `${low} low severity vulnerabilities`);
|
||||
}
|
||||
|
||||
if (critical === 0 && high === 0 && moderate === 0 && low === 0) {
|
||||
log('PASS', 'No known vulnerabilities in dependencies');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log('INFO', 'npm audit completed with findings (check above)');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 3. CHECK AUTHENTICATION & AUTHORIZATION
|
||||
// ============================================================================
|
||||
function checkAuthSecurity() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '3. Authentication & Authorization' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
// Check JWT secret strength
|
||||
const jwtUtilPath = path.join(__dirname, '../src/utils/jwt.util.js');
|
||||
if (fs.existsSync(jwtUtilPath)) {
|
||||
const jwtContent = fs.readFileSync(jwtUtilPath, 'utf8');
|
||||
|
||||
// Check if JWT_SECRET is required
|
||||
if (!jwtContent.includes('JWT_SECRET')) {
|
||||
addIssue('critical', 'JWT secret not configured',
|
||||
'JWT_SECRET environment variable not used',
|
||||
'Configure JWT_SECRET in environment variables');
|
||||
log('CRITICAL', 'JWT_SECRET not found in jwt.util.js');
|
||||
} else {
|
||||
log('PASS', 'JWT uses environment variable for secret');
|
||||
}
|
||||
|
||||
// Check for secure JWT options
|
||||
if (!jwtContent.includes('expiresIn')) {
|
||||
addIssue('medium', 'JWT expiration not set',
|
||||
'Tokens may not expire',
|
||||
'Set expiresIn option for JWT tokens');
|
||||
log('MEDIUM', 'JWT expiration not configured');
|
||||
} else {
|
||||
log('PASS', 'JWT expiration configured');
|
||||
}
|
||||
}
|
||||
|
||||
// Check password hashing
|
||||
const userModelPath = path.join(__dirname, '../src/models/User.model.js');
|
||||
if (fs.existsSync(userModelPath)) {
|
||||
const userContent = fs.readFileSync(userModelPath, 'utf8');
|
||||
|
||||
if (!userContent.includes('bcrypt')) {
|
||||
addIssue('critical', 'Passwords not hashed',
|
||||
'bcrypt not found in User model',
|
||||
'Use bcrypt to hash passwords with salt rounds >= 10');
|
||||
log('CRITICAL', 'Password hashing (bcrypt) not found');
|
||||
} else {
|
||||
log('PASS', 'Passwords are hashed with bcrypt');
|
||||
|
||||
// Check salt rounds
|
||||
const saltRoundsMatch = userContent.match(/bcrypt\.hash\([^,]+,\s*(\d+)/);
|
||||
if (saltRoundsMatch) {
|
||||
const rounds = parseInt(saltRoundsMatch[1]);
|
||||
if (rounds < 10) {
|
||||
addIssue('medium', 'Weak bcrypt salt rounds',
|
||||
`Salt rounds set to ${rounds}, should be >= 10`,
|
||||
'Increase bcrypt salt rounds to at least 10');
|
||||
log('MEDIUM', `Bcrypt salt rounds: ${rounds} (should be >= 10)`);
|
||||
} else {
|
||||
log('PASS', `Bcrypt salt rounds: ${rounds}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check rate limiting
|
||||
const serverPath = path.join(__dirname, '../src/server.js');
|
||||
if (fs.existsSync(serverPath)) {
|
||||
const serverContent = fs.readFileSync(serverPath, 'utf8');
|
||||
|
||||
if (!serverContent.includes('rateLimit') && !serverContent.includes('express-rate-limit')) {
|
||||
addIssue('high', 'No rate limiting',
|
||||
'Rate limiting not implemented',
|
||||
'Add express-rate-limit to prevent brute force attacks');
|
||||
log('HIGH', 'Rate limiting not found');
|
||||
} else {
|
||||
log('PASS', 'Rate limiting implemented');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 4. CHECK INPUT VALIDATION
|
||||
// ============================================================================
|
||||
function checkInputValidation() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '4. Input Validation & Sanitization' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
// Check for validation middleware
|
||||
const validationPath = path.join(__dirname, '../src/middleware/validation.middleware.js');
|
||||
if (!fs.existsSync(validationPath)) {
|
||||
addIssue('high', 'No validation middleware',
|
||||
'Input validation middleware not found',
|
||||
'Create validation middleware to sanitize user inputs');
|
||||
log('HIGH', 'Validation middleware not found');
|
||||
} else {
|
||||
log('PASS', 'Validation middleware exists');
|
||||
|
||||
const validationContent = fs.readFileSync(validationPath, 'utf8');
|
||||
|
||||
// Check for common validation functions
|
||||
const validations = ['validateEmail', 'validateRequired', 'validateObjectId'];
|
||||
validations.forEach(fn => {
|
||||
if (validationContent.includes(fn)) {
|
||||
log('PASS', `${fn} validation implemented`);
|
||||
} else {
|
||||
log('LOW', `${fn} validation not found`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check for NoSQL injection protection
|
||||
const controllersDir = path.join(__dirname, '../src/controllers');
|
||||
if (fs.existsSync(controllersDir)) {
|
||||
try {
|
||||
const grepCmd = `grep -r "\\$where\\|\\$ne\\|\\$gt" ${controllersDir} || true`;
|
||||
const result = execSync(grepCmd, { encoding: 'utf8' });
|
||||
if (result.trim()) {
|
||||
addIssue('medium', 'Potential NoSQL injection vectors',
|
||||
'Direct use of MongoDB operators in controllers',
|
||||
'Sanitize user inputs before using in database queries');
|
||||
log('MEDIUM', 'Potential NoSQL injection vectors found');
|
||||
} else {
|
||||
log('PASS', 'No obvious NoSQL injection vectors');
|
||||
}
|
||||
} catch (err) {
|
||||
log('PASS', 'No obvious NoSQL injection vectors');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 5. CHECK SECURITY HEADERS
|
||||
// ============================================================================
|
||||
function checkSecurityHeaders() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '5. Security Headers' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
const serverPath = path.join(__dirname, '../src/server.js');
|
||||
if (fs.existsSync(serverPath)) {
|
||||
const serverContent = fs.readFileSync(serverPath, 'utf8');
|
||||
|
||||
if (!serverContent.includes('helmet')) {
|
||||
addIssue('high', 'helmet middleware not used',
|
||||
'Security headers not configured',
|
||||
'Add helmet middleware to set security headers');
|
||||
log('HIGH', 'helmet middleware not found');
|
||||
} else {
|
||||
log('PASS', 'helmet middleware configured');
|
||||
}
|
||||
|
||||
if (!serverContent.includes('cors')) {
|
||||
addIssue('medium', 'CORS not configured',
|
||||
'CORS middleware not found',
|
||||
'Configure CORS to restrict cross-origin requests');
|
||||
log('MEDIUM', 'CORS not configured');
|
||||
} else {
|
||||
log('PASS', 'CORS configured');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 6. CHECK FILE PERMISSIONS
|
||||
// ============================================================================
|
||||
function checkFilePermissions() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '6. File Permissions' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
const sensitiveFiles = [
|
||||
'.env',
|
||||
'package.json',
|
||||
'src/config/app.config.js'
|
||||
];
|
||||
|
||||
sensitiveFiles.forEach(file => {
|
||||
const filePath = path.join(__dirname, '..', file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const stats = fs.statSync(filePath);
|
||||
const mode = (stats.mode & parseInt('777', 8)).toString(8);
|
||||
|
||||
if (file === '.env' && mode !== '600') {
|
||||
addIssue('medium', `.env file permissions too permissive`,
|
||||
`File permissions: ${mode} (should be 600)`,
|
||||
`chmod 600 ${file}`);
|
||||
log('MEDIUM', `.env permissions: ${mode} (should be 600)`);
|
||||
} else {
|
||||
log('PASS', `${file} permissions: ${mode}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 7. CHECK LOGGING & ERROR HANDLING
|
||||
// ============================================================================
|
||||
function checkLoggingAndErrors() {
|
||||
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.cyan + '7. Logging & Error Handling' + colors.reset);
|
||||
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
||||
|
||||
const errorMiddlewarePath = path.join(__dirname, '../src/middleware/error.middleware.js');
|
||||
if (!fs.existsSync(errorMiddlewarePath)) {
|
||||
addIssue('medium', 'No error handling middleware',
|
||||
'Error middleware not found',
|
||||
'Create error handling middleware to sanitize error messages');
|
||||
log('MEDIUM', 'Error handling middleware not found');
|
||||
} else {
|
||||
const errorContent = fs.readFileSync(errorMiddlewarePath, 'utf8');
|
||||
|
||||
// Check that stack traces are not exposed in production
|
||||
if (errorContent.includes('stack') && !errorContent.includes('NODE_ENV')) {
|
||||
addIssue('medium', 'Stack traces may be exposed',
|
||||
'Error handler may expose stack traces in production',
|
||||
'Only show stack traces in development environment');
|
||||
log('MEDIUM', 'Stack traces may be exposed in production');
|
||||
} else {
|
||||
log('PASS', 'Error handling configured properly');
|
||||
}
|
||||
}
|
||||
|
||||
// Check logger configuration
|
||||
const loggerPath = path.join(__dirname, '../src/utils/logger.util.js');
|
||||
if (fs.existsSync(loggerPath)) {
|
||||
const loggerContent = fs.readFileSync(loggerPath, 'utf8');
|
||||
|
||||
if (loggerContent.includes('password') || loggerContent.includes('token')) {
|
||||
log('LOW', 'Logger may log sensitive data - review logger.util.js');
|
||||
} else {
|
||||
log('PASS', 'Logger configuration looks safe');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GENERATE REPORT
|
||||
// ============================================================================
|
||||
function generateReport() {
|
||||
console.log('\n' + colors.magenta + '='.repeat(80) + colors.reset);
|
||||
console.log(colors.magenta + 'SECURITY AUDIT SUMMARY' + colors.reset);
|
||||
console.log(colors.magenta + '='.repeat(80) + colors.reset);
|
||||
|
||||
const totalIssues = issues.critical.length + issues.high.length +
|
||||
issues.medium.length + issues.low.length;
|
||||
|
||||
console.log(`\n${colors.cyan}Total Issues Found: ${totalIssues}${colors.reset}`);
|
||||
console.log(` ${colors.red}Critical: ${issues.critical.length}${colors.reset}`);
|
||||
console.log(` ${colors.red}High: ${issues.high.length}${colors.reset}`);
|
||||
console.log(` ${colors.yellow}Medium: ${issues.medium.length}${colors.reset}`);
|
||||
console.log(` ${colors.cyan}Low: ${issues.low.length}${colors.reset}`);
|
||||
|
||||
// Print critical issues
|
||||
if (issues.critical.length > 0) {
|
||||
console.log('\n' + colors.red + 'CRITICAL ISSUES:' + colors.reset);
|
||||
issues.critical.forEach((issue, i) => {
|
||||
console.log(`\n${i + 1}. ${colors.red}${issue.title}${colors.reset}`);
|
||||
console.log(` ${issue.description}`);
|
||||
console.log(` ${colors.green}→ ${issue.remediation}${colors.reset}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Print high issues
|
||||
if (issues.high.length > 0) {
|
||||
console.log('\n' + colors.red + 'HIGH SEVERITY ISSUES:' + colors.reset);
|
||||
issues.high.forEach((issue, i) => {
|
||||
console.log(`\n${i + 1}. ${colors.red}${issue.title}${colors.reset}`);
|
||||
console.log(` ${issue.description}`);
|
||||
console.log(` ${colors.green}→ ${issue.remediation}${colors.reset}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Print medium issues
|
||||
if (issues.medium.length > 0) {
|
||||
console.log('\n' + colors.yellow + 'MEDIUM SEVERITY ISSUES:' + colors.reset);
|
||||
issues.medium.forEach((issue, i) => {
|
||||
console.log(`\n${i + 1}. ${colors.yellow}${issue.title}${colors.reset}`);
|
||||
console.log(` ${issue.description}`);
|
||||
console.log(` ${colors.green}→ ${issue.remediation}${colors.reset}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Overall status
|
||||
console.log('\n' + colors.magenta + '='.repeat(80) + colors.reset);
|
||||
if (issues.critical.length === 0 && issues.high.length === 0) {
|
||||
console.log(colors.green + '✓ No critical or high severity issues found' + colors.reset);
|
||||
} else {
|
||||
console.log(colors.red + '✗ Critical or high severity issues require immediate attention' + colors.reset);
|
||||
}
|
||||
console.log(colors.magenta + '='.repeat(80) + colors.reset + '\n');
|
||||
|
||||
// Exit with error code if critical/high issues found
|
||||
process.exit((issues.critical.length + issues.high.length) > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN
|
||||
// ============================================================================
|
||||
function main() {
|
||||
console.log(colors.magenta + '\n' + '='.repeat(80));
|
||||
console.log('TRACTATUS SECURITY AUDIT');
|
||||
console.log('Checking for common security vulnerabilities and best practices');
|
||||
console.log('='.repeat(80) + colors.reset);
|
||||
|
||||
checkEnvironmentVariables();
|
||||
checkDependencies();
|
||||
checkAuthSecurity();
|
||||
checkInputValidation();
|
||||
checkSecurityHeaders();
|
||||
checkFilePermissions();
|
||||
checkLoggingAndErrors();
|
||||
|
||||
generateReport();
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Seed Script: Add Architectural Safeguards Document to Database
|
||||
* Adds both MD and PDF versions of the Architectural Safeguards Against LLM Hierarchical Dominance document
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { markdownToHtml, extractTOC, generateSlug } = require('../src/utils/markdown.util');
|
||||
|
||||
async function seedDocument() {
|
||||
try {
|
||||
console.log('\n=== Seeding Architectural Safeguards Document ===\n');
|
||||
|
||||
// Connect to database
|
||||
await connect();
|
||||
|
||||
// Read the prose markdown file
|
||||
const mdPath = path.join(__dirname, '..', 'docs', 'research', 'ARCHITECTURAL-SAFEGUARDS-Against-LLM-Hierarchical-Dominance-Prose.md');
|
||||
const rawContent = await fs.readFile(mdPath, 'utf-8');
|
||||
|
||||
console.log('✓ Read markdown file');
|
||||
|
||||
// Convert to HTML
|
||||
const htmlContent = markdownToHtml(rawContent);
|
||||
|
||||
// Extract table of contents
|
||||
const tableOfContents = extractTOC(rawContent);
|
||||
|
||||
console.log('✓ Converted to HTML and extracted TOC');
|
||||
|
||||
// Generate slug
|
||||
const slug = 'architectural-safeguards-against-llm-hierarchical-dominance-prose';
|
||||
|
||||
// Check if document already exists
|
||||
const existing = await Document.findBySlug(slug);
|
||||
|
||||
if (existing) {
|
||||
console.log(`\n⚠️ Document already exists with slug: ${slug}`);
|
||||
console.log(' Delete it first or use a different slug.');
|
||||
await close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Create document object
|
||||
const doc = {
|
||||
title: 'Architectural Safeguards Against LLM Hierarchical Dominance',
|
||||
slug: slug,
|
||||
quadrant: null, // Research document, not bound to specific quadrant
|
||||
persistence: 'HIGH',
|
||||
audience: 'leader', // Target audience: leaders, decision-makers
|
||||
visibility: 'public',
|
||||
category: 'research-theory', // Research and theory category
|
||||
order: 10, // Higher priority (lower number = higher priority in display)
|
||||
content_html: htmlContent,
|
||||
content_markdown: rawContent,
|
||||
toc: tableOfContents,
|
||||
security_classification: {
|
||||
contains_credentials: false,
|
||||
contains_financial_info: false,
|
||||
contains_vulnerability_info: false,
|
||||
contains_infrastructure_details: false,
|
||||
requires_authentication: false
|
||||
},
|
||||
metadata: {
|
||||
author: 'Agentic Governance Research Team',
|
||||
version: '1.0',
|
||||
document_code: null,
|
||||
related_documents: [
|
||||
'executive-summary-pluralistic-deliberation-in-tractatus',
|
||||
'phase-1-implementation-tickets',
|
||||
'research-paper-outline-pluralistic-deliberation'
|
||||
],
|
||||
tags: [
|
||||
'ai-safety',
|
||||
'llm-governance',
|
||||
'value-pluralism',
|
||||
'deliberative-ai',
|
||||
'hierarchical-dominance',
|
||||
'pluralistic-deliberation',
|
||||
'research'
|
||||
]
|
||||
},
|
||||
translations: {},
|
||||
search_index: rawContent.toLowerCase(),
|
||||
download_formats: {
|
||||
pdf: '/docs/research/ARCHITECTURAL-SAFEGUARDS-Against-LLM-Hierarchical-Dominance-Prose.pdf',
|
||||
markdown: '/docs/research/ARCHITECTURAL-SAFEGUARDS-Against-LLM-Hierarchical-Dominance-Prose.md'
|
||||
}
|
||||
};
|
||||
|
||||
// Create document
|
||||
const createdDoc = await Document.create(doc);
|
||||
|
||||
console.log('\n✓ Document created successfully!');
|
||||
console.log(` Title: ${createdDoc.title}`);
|
||||
console.log(` Slug: ${createdDoc.slug}`);
|
||||
console.log(` Category: ${createdDoc.category}`);
|
||||
console.log(` Audience: ${createdDoc.audience}`);
|
||||
console.log(` PDF: ${doc.download_formats.pdf}`);
|
||||
console.log(` Markdown: ${doc.download_formats.markdown}`);
|
||||
|
||||
console.log('\n✓ Document is now available at:');
|
||||
console.log(` https://agenticgovernance.digital/docs.html?doc=${slug}`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error seeding document:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
seedDocument();
|
||||
}
|
||||
|
||||
module.exports = seedDocument;
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
/**
|
||||
* Seed First Blog Post - Framework Overview
|
||||
* Creates the inaugural blog post introducing the Tractatus framework
|
||||
*/
|
||||
|
||||
const { getCollection, connect, close } = require('../src/utils/db.util');
|
||||
|
||||
const BLOG_POST = {
|
||||
title: 'Introducing the Tractatus Framework: Architectural Safety for Production AI',
|
||||
slug: 'introducing-tractatus-framework',
|
||||
author: {
|
||||
type: 'human',
|
||||
name: 'John Stroh'
|
||||
},
|
||||
content: `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 that work together to prevent common failure modes in AI deployments.
|
||||
|
||||
## The Core Problem
|
||||
|
||||
Current AI safety approaches rely on aspirational prompts, documentation files, and trust that the AI will "remember" to follow guidelines. This breaks down under context pressure, session continuations, and real-world complexity. We've seen this failure mode repeatedly in production deployments.
|
||||
|
||||
## The Tractatus Solution
|
||||
|
||||
Instead of asking AI to remember governance rules, **Tractatus enforces them architecturally**. The framework consists of six mandatory services that operate continuously:
|
||||
|
||||
### 1. **BoundaryEnforcer**
|
||||
Blocks decisions that cross into values territory before they're executed. AI systems should never autonomously decide questions of ethics, user agency, or irreversible consequences.
|
||||
|
||||
### 2. **InstructionPersistenceClassifier**
|
||||
Classifies every explicit instruction by:
|
||||
- **Quadrant**: Strategic, Operational, Tactical, System, or Stochastic
|
||||
- **Persistence level**: HIGH, MEDIUM, LOW, or VARIABLE
|
||||
- **Temporal scope**: Project-wide, phase-specific, or session-only
|
||||
|
||||
Only instructions with high explicitness scores (≥0.6) are stored persistently.
|
||||
|
||||
### 3. **CrossReferenceValidator**
|
||||
Checks proposed changes against the persistent instruction database before execution. Prevents pattern bias and instruction conflicts.
|
||||
|
||||
### 4. **ContextPressureMonitor**
|
||||
Tracks token budget usage and context window pressure. Warns before degradation occurs, runs at 25% intervals (50k tokens).
|
||||
|
||||
### 5. **MetacognitiveVerifier**
|
||||
For complex operations (>3 files, >5 steps, architecture changes), verifies:
|
||||
- Alignment with goals
|
||||
- Coherence across components
|
||||
- Completeness of implementation
|
||||
- Safety considerations
|
||||
- Alternative approaches
|
||||
|
||||
### 6. **PluralisticDeliberationOrchestrator**
|
||||
When values decisions are unavoidable, facilitates multi-stakeholder deliberation without imposing hierarchy. Ensures affected communities have voice in governance.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
The Tractatus framework represents a fundamental shift in AI governance:
|
||||
|
||||
**From:** "We hope the AI remembers to ask"
|
||||
**To:** "The architecture prevents unauthorized decisions"
|
||||
|
||||
**From:** "Prompts as documentation"
|
||||
**To:** "Enforcement at the architectural layer"
|
||||
|
||||
**From:** "Trust but don't verify"
|
||||
**To:** "Systematic validation and auditing"
|
||||
|
||||
## Validated in Production
|
||||
|
||||
The framework was developed and validated through 6 months of real-world deployment on the Tractatus website project (~500 sessions with Claude Code). It successfully:
|
||||
|
||||
- Prevented pattern bias incidents that previously caused production issues
|
||||
- Maintained governance across session continuations
|
||||
- Escalated all values decisions to human approval
|
||||
- Detected framework fade before it caused problems
|
||||
|
||||
This validation occurred in a controlled, single-project context. We're now expanding to broader testing with diverse deployment scenarios.
|
||||
|
||||
## Target Deployments
|
||||
|
||||
Tractatus is designed for production AI systems where safety matters:
|
||||
|
||||
- **Claude Code deployments** (primary target)
|
||||
- **Agentic AI systems** with autonomous decision-making
|
||||
- **High-stakes applications** where errors have real consequences
|
||||
- **Regulated industries** requiring audit trails and governance
|
||||
|
||||
The framework is conceptually portable to other LLMs (GPT-4, Gemini, local models) but currently optimized for Claude Code's architecture.
|
||||
|
||||
## Getting Started
|
||||
|
||||
**For Researchers:** Explore our [comprehensive documentation](/docs.html) including theoretical foundations, validation methodology, and research papers.
|
||||
|
||||
**For Implementers:** Download the [quickstart package](/downloads/tractatus-quickstart.tar.gz) with Docker deployment, configuration templates, and integration guides.
|
||||
|
||||
**For Leaders:** Read our [business case](/leader.html) explaining ROI, risk mitigation, and organizational benefits.
|
||||
|
||||
## What's Next
|
||||
|
||||
We're committed to transparent, community-driven development:
|
||||
|
||||
- **Open source** under Apache 2.0 license
|
||||
- **Public roadmap** with quarterly milestones
|
||||
- **Research partnerships** with academic institutions
|
||||
- **Case study contributions** from real deployments
|
||||
|
||||
The Tractatus framework is early-stage research, not production-scale validation. We're honest about what we know, what we don't know, and what we're actively investigating.
|
||||
|
||||
## Join the Community
|
||||
|
||||
- 📖 [Read the full documentation](/docs.html)
|
||||
- 🔬 [Browse our research papers](/downloads/structural-governance-for-agentic-ai-tractatus-inflection-point.pdf)
|
||||
- 💬 [Join the discussion on GitHub](https://github.com/AgenticGovernance/tractatus-framework)
|
||||
- 📧 [Contact us](/media-inquiry.html) with questions or collaboration inquiries
|
||||
|
||||
---
|
||||
|
||||
*This post introduces the Tractatus framework at a conceptual level. For technical implementation details, see our [Technical Architecture](/docs.html) documentation.*`,
|
||||
excerpt: 'Introducing Tractatus: an architectural governance framework for production AI that enforces safety through six mandatory services, not aspirational prompts.',
|
||||
category: 'Framework Updates',
|
||||
status: 'published',
|
||||
published_at: new Date(),
|
||||
moderation: {
|
||||
ai_analysis: null,
|
||||
human_reviewer: 'John Stroh',
|
||||
review_notes: 'Inaugural blog post - framework introduction',
|
||||
approved_at: new Date()
|
||||
},
|
||||
tractatus_classification: {
|
||||
quadrant: 'STRATEGIC',
|
||||
values_sensitive: false,
|
||||
requires_strategic_review: false
|
||||
},
|
||||
tags: [
|
||||
'framework',
|
||||
'introduction',
|
||||
'ai-safety',
|
||||
'governance',
|
||||
'architecture',
|
||||
'claude-code',
|
||||
'production-ai'
|
||||
],
|
||||
view_count: 0,
|
||||
engagement: {
|
||||
shares: 0,
|
||||
comments: 0
|
||||
}
|
||||
};
|
||||
|
||||
async function seedBlogPost() {
|
||||
try {
|
||||
console.log('🌱 Seeding first blog post...');
|
||||
|
||||
await connect();
|
||||
const collection = await getCollection('blog_posts');
|
||||
|
||||
// Check if post already exists
|
||||
const existing = await collection.findOne({ slug: BLOG_POST.slug });
|
||||
if (existing) {
|
||||
console.log('📝 Blog post already exists:', BLOG_POST.slug);
|
||||
console.log(' To update, delete it first or change the slug');
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert the blog post
|
||||
const result = await collection.insertOne(BLOG_POST);
|
||||
console.log('✅ Blog post created successfully');
|
||||
console.log(' ID:', result.insertedId);
|
||||
console.log(' Slug:', BLOG_POST.slug);
|
||||
console.log(' Title:', BLOG_POST.title);
|
||||
console.log(' Status:', BLOG_POST.status);
|
||||
console.log(' Tags:', BLOG_POST.tags.join(', '));
|
||||
console.log('');
|
||||
console.log('📍 View at: http://localhost:9000/blog-post.html?slug=' + BLOG_POST.slug);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error seeding blog post:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
seedBlogPost()
|
||||
.then(() => {
|
||||
console.log('\n✨ Seeding complete');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n💥 Seeding failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { seedBlogPost, BLOG_POST };
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Seed Projects Script
|
||||
* Creates sample projects and their variable values for development/testing
|
||||
*
|
||||
* Usage: npm run seed:projects
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect: connectDb, close: closeDb } = require('../src/utils/db.util');
|
||||
const { connect: connectMongoose, close: closeMongoose } = require('../src/utils/mongoose.util');
|
||||
const Project = require('../src/models/Project.model');
|
||||
const VariableValue = require('../src/models/VariableValue.model');
|
||||
const logger = require('../src/utils/logger.util');
|
||||
|
||||
/**
|
||||
* Sample projects with their variable values
|
||||
*/
|
||||
const sampleProjects = [
|
||||
{
|
||||
id: 'tractatus',
|
||||
name: 'Tractatus AI Safety Framework',
|
||||
description: 'The Tractatus website - multi-project governance system with AI safety framework',
|
||||
techStack: {
|
||||
framework: 'Express.js',
|
||||
database: 'MongoDB',
|
||||
frontend: 'Vanilla JavaScript',
|
||||
css: 'Tailwind CSS'
|
||||
},
|
||||
repositoryUrl: 'https://github.com/example/tractatus',
|
||||
metadata: {
|
||||
environment: 'production',
|
||||
domain: 'tractatus.org'
|
||||
},
|
||||
active: true,
|
||||
variables: [
|
||||
{ name: 'DB_NAME', value: 'tractatus_prod', description: 'Production database name', category: 'database' },
|
||||
{ name: 'DB_PORT', value: '27017', description: 'MongoDB port', category: 'database' },
|
||||
{ name: 'APP_PORT', value: '9000', description: 'Application HTTP port', category: 'config' },
|
||||
{ name: 'API_BASE_URL', value: 'https://tractatus.org/api', description: 'Base URL for API', category: 'url' },
|
||||
{ name: 'SESSION_SECRET', value: 'PROD_SECRET_KEY', description: 'Session encryption key', category: 'security' },
|
||||
{ name: 'LOG_LEVEL', value: 'info', description: 'Logging verbosity', category: 'config' },
|
||||
{ name: 'MAX_UPLOAD_SIZE', value: '10485760', description: 'Max file upload size (10MB)', category: 'config' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'family-history',
|
||||
name: 'Family History Archive',
|
||||
description: 'Digital family archive with document management and OCR capabilities',
|
||||
techStack: {
|
||||
framework: 'Express.js',
|
||||
database: 'MongoDB',
|
||||
frontend: 'React',
|
||||
css: 'Tailwind CSS',
|
||||
storage: 'S3-compatible'
|
||||
},
|
||||
repositoryUrl: 'https://github.com/example/family-history',
|
||||
metadata: {
|
||||
environment: 'development',
|
||||
features: ['OCR', 'Document Management', 'Search']
|
||||
},
|
||||
active: true,
|
||||
variables: [
|
||||
{ name: 'DB_NAME', value: 'family_history_dev', description: 'Development database name', category: 'database' },
|
||||
{ name: 'DB_PORT', value: '27017', description: 'MongoDB port', category: 'database' },
|
||||
{ name: 'APP_PORT', value: '3000', description: 'Application HTTP port', category: 'config' },
|
||||
{ name: 'API_BASE_URL', value: 'http://localhost:3000/api', description: 'Base URL for API', category: 'url' },
|
||||
{ name: 'STORAGE_BUCKET', value: 'family-history-documents', description: 'S3 bucket name', category: 'path' },
|
||||
{ name: 'OCR_ENABLED', value: 'true', description: 'Enable OCR processing', category: 'feature_flag', dataType: 'boolean' },
|
||||
{ name: 'MAX_UPLOAD_SIZE', value: '52428800', description: 'Max file upload size (50MB)', category: 'config' },
|
||||
{ name: 'LOG_LEVEL', value: 'debug', description: 'Logging verbosity', category: 'config' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'sydigital',
|
||||
name: 'SyDigital Platform',
|
||||
description: 'Digital transformation platform for enterprise solutions',
|
||||
techStack: {
|
||||
framework: 'Next.js',
|
||||
database: 'PostgreSQL',
|
||||
frontend: 'React',
|
||||
css: 'Styled Components',
|
||||
cache: 'Redis'
|
||||
},
|
||||
repositoryUrl: 'https://github.com/example/sydigital',
|
||||
metadata: {
|
||||
environment: 'staging',
|
||||
tier: 'enterprise'
|
||||
},
|
||||
active: true,
|
||||
variables: [
|
||||
{ name: 'DB_NAME', value: 'sydigital_staging', description: 'Staging database name', category: 'database' },
|
||||
{ name: 'DB_PORT', value: '5432', description: 'PostgreSQL port', category: 'database', dataType: 'number' },
|
||||
{ name: 'APP_PORT', value: '3001', description: 'Application HTTP port', category: 'config' },
|
||||
{ name: 'API_BASE_URL', value: 'https://staging.sydigital.com/api', description: 'Base URL for API', category: 'url' },
|
||||
{ name: 'REDIS_URL', value: 'redis://localhost:6379', description: 'Redis connection URL', category: 'url' },
|
||||
{ name: 'CACHE_TTL', value: '3600', description: 'Cache TTL in seconds', category: 'config', dataType: 'number' },
|
||||
{ name: 'API_RATE_LIMIT', value: '1000', description: 'API requests per hour', category: 'config', dataType: 'number' },
|
||||
{ name: 'LOG_LEVEL', value: 'warn', description: 'Logging verbosity', category: 'config' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'example-project',
|
||||
name: 'Example Project',
|
||||
description: 'Template project for testing and demonstration purposes',
|
||||
techStack: {
|
||||
framework: 'Express.js',
|
||||
database: 'MongoDB',
|
||||
frontend: 'Vanilla JavaScript'
|
||||
},
|
||||
repositoryUrl: null,
|
||||
metadata: {
|
||||
environment: 'development',
|
||||
purpose: 'testing'
|
||||
},
|
||||
active: false, // Inactive to demonstrate filtering
|
||||
variables: [
|
||||
{ name: 'DB_NAME', value: 'example_db', description: 'Example database', category: 'database' },
|
||||
{ name: 'DB_PORT', value: '27017', description: 'MongoDB port', category: 'database' },
|
||||
{ name: 'APP_PORT', value: '8080', description: 'Application port', category: 'config' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
async function seedProjects() {
|
||||
try {
|
||||
console.log('\n=== Tractatus Projects & Variables Seed ===\n');
|
||||
|
||||
// Connect to database (both native and Mongoose)
|
||||
await connectDb();
|
||||
await connectMongoose();
|
||||
|
||||
// Get existing projects count
|
||||
const existingCount = await Project.countDocuments();
|
||||
|
||||
if (existingCount > 0) {
|
||||
console.log(`⚠️ Found ${existingCount} existing project(s) in database.`);
|
||||
console.log('This will DELETE ALL existing projects and variables!');
|
||||
console.log('Continue? (yes/no): ');
|
||||
|
||||
// Read from stdin
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const answer = await new Promise(resolve => {
|
||||
rl.question('', resolve);
|
||||
});
|
||||
rl.close();
|
||||
|
||||
if (answer.toLowerCase() !== 'yes') {
|
||||
console.log('Cancelled. No changes made.');
|
||||
await cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete all existing projects and variables
|
||||
await Project.deleteMany({});
|
||||
await VariableValue.deleteMany({});
|
||||
console.log('✅ Deleted all existing projects and variables.\n');
|
||||
}
|
||||
|
||||
// Create projects and variables
|
||||
let createdCount = 0;
|
||||
let variableCount = 0;
|
||||
|
||||
for (const projectData of sampleProjects) {
|
||||
const { variables, ...projectInfo } = projectData;
|
||||
|
||||
// Create project
|
||||
const project = new Project({
|
||||
...projectInfo,
|
||||
createdBy: 'seed-script',
|
||||
updatedBy: 'seed-script'
|
||||
});
|
||||
|
||||
await project.save();
|
||||
createdCount++;
|
||||
|
||||
console.log(`✅ Created project: ${project.name} (${project.id})`);
|
||||
|
||||
// Create variables for this project
|
||||
if (variables && variables.length > 0) {
|
||||
for (const varData of variables) {
|
||||
const variable = new VariableValue({
|
||||
projectId: project.id,
|
||||
variableName: varData.name,
|
||||
value: varData.value,
|
||||
description: varData.description || '',
|
||||
category: varData.category || 'other',
|
||||
dataType: varData.dataType || 'string',
|
||||
active: true,
|
||||
createdBy: 'seed-script',
|
||||
updatedBy: 'seed-script'
|
||||
});
|
||||
|
||||
await variable.save();
|
||||
variableCount++;
|
||||
}
|
||||
|
||||
console.log(` └─ Created ${variables.length} variable(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Seed Complete ===');
|
||||
console.log(`✅ Created ${createdCount} projects`);
|
||||
console.log(`✅ Created ${variableCount} variables`);
|
||||
console.log('\nYou can now:');
|
||||
console.log(' - View projects: GET /api/admin/projects');
|
||||
console.log(' - View variables: GET /api/admin/projects/:projectId/variables');
|
||||
console.log(' - Test substitution: GET /api/admin/rules?projectId=tractatus');
|
||||
console.log('');
|
||||
|
||||
logger.info(`Projects seeded: ${createdCount} projects, ${variableCount} variables`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error seeding projects:', error.message);
|
||||
logger.error('Projects seed error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
await closeDb();
|
||||
await closeMongoose();
|
||||
}
|
||||
|
||||
// Handle Ctrl+C
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n\n👋 Cancelled by user');
|
||||
await cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
seedProjects();
|
||||
}
|
||||
|
||||
module.exports = seedProjects;
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/**
|
||||
* Seed Scaling Blog Post
|
||||
* "How to Scale Tractatus: Breaking the Chicken-and-Egg Problem"
|
||||
*/
|
||||
|
||||
const { getCollection, connect, close } = require('../src/utils/db.util');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Read the markdown content
|
||||
const markdown = fs.readFileSync(
|
||||
path.join(__dirname, '../docs/outreach/Blog-Article-Scaling-Tractatus.md'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Extract content (skip frontmatter-style metadata at top)
|
||||
const contentStart = markdown.indexOf('---', markdown.indexOf('---') + 3) + 3;
|
||||
const content = markdown.substring(contentStart).trim();
|
||||
|
||||
const BLOG_POST = {
|
||||
title: 'How to Scale Tractatus: Breaking the Chicken-and-Egg Problem',
|
||||
slug: 'scaling-tractatus-roadmap',
|
||||
author: {
|
||||
type: 'human',
|
||||
name: 'John Stroh'
|
||||
},
|
||||
content: content,
|
||||
excerpt: 'Every governance framework faces the same chicken-and-egg problem: need deployments to validate, need validation to deploy. Here\'s our staged roadmap for breaking the cycle and scaling Tractatus from proof-of-concept to industry adoption.',
|
||||
category: 'Implementation',
|
||||
status: 'published',
|
||||
published_at: new Date('2025-10-20'),
|
||||
moderation: {
|
||||
ai_analysis: null,
|
||||
human_reviewer: 'John Stroh',
|
||||
review_notes: 'Scaling roadmap and pilot partner call-to-action',
|
||||
approved_at: new Date()
|
||||
},
|
||||
tractatus_classification: {
|
||||
quadrant: 'STRATEGIC',
|
||||
values_sensitive: false,
|
||||
requires_strategic_review: false
|
||||
},
|
||||
tags: [
|
||||
'scaling',
|
||||
'roadmap',
|
||||
'implementation',
|
||||
'enterprise',
|
||||
'governance',
|
||||
'pilot-partners',
|
||||
'stage-2',
|
||||
'chicken-and-egg'
|
||||
],
|
||||
view_count: 0,
|
||||
engagement: {
|
||||
shares: 0,
|
||||
comments: 0
|
||||
}
|
||||
};
|
||||
|
||||
async function seedBlogPost() {
|
||||
try {
|
||||
console.log('🌱 Seeding scaling blog post...');
|
||||
|
||||
await connect();
|
||||
const collection = await getCollection('blog_posts');
|
||||
|
||||
// Check if post already exists
|
||||
const existing = await collection.findOne({ slug: BLOG_POST.slug });
|
||||
if (existing) {
|
||||
console.log('📝 Blog post already exists:', BLOG_POST.slug);
|
||||
console.log(' Deleting existing post and creating new one...');
|
||||
await collection.deleteOne({ slug: BLOG_POST.slug });
|
||||
}
|
||||
|
||||
// Insert the blog post
|
||||
const result = await collection.insertOne(BLOG_POST);
|
||||
console.log('✅ Blog post created successfully');
|
||||
console.log(' ID:', result.insertedId);
|
||||
console.log(' Slug:', BLOG_POST.slug);
|
||||
console.log(' Title:', BLOG_POST.title);
|
||||
console.log(' Status:', BLOG_POST.status);
|
||||
console.log(' Category:', BLOG_POST.category);
|
||||
console.log(' Tags:', BLOG_POST.tags.join(', '));
|
||||
console.log('');
|
||||
console.log('📍 View at: http://localhost:9000/blog-post.html?slug=' + BLOG_POST.slug);
|
||||
console.log('📍 Blog list: http://localhost:9000/blog.html');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error seeding blog post:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
seedBlogPost()
|
||||
.then(() => {
|
||||
console.log('\n✨ Seeding complete');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\n💥 Seeding failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { seedBlogPost, BLOG_POST };
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Sync Instructions to Database (v3 - Clean programmatic + CLI support)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const GovernanceRule = require('../src/models/GovernanceRule.model');
|
||||
|
||||
const INSTRUCTION_FILE = path.join(__dirname, '../.claude/instruction-history.json');
|
||||
const ORPHAN_BACKUP = path.join(__dirname, '../.claude/backups/orphaned-rules-' + new Date().toISOString().replace(/:/g, '-') + '.json');
|
||||
|
||||
// Parse CLI args (only used when run from command line)
|
||||
const args = process.argv.slice(2);
|
||||
const cliDryRun = args.includes('--dry-run');
|
||||
const cliForce = args.includes('--force');
|
||||
const cliSilent = args.includes('--silent');
|
||||
|
||||
const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' };
|
||||
|
||||
// Log functions (will respect silent flag passed to main function)
|
||||
let SILENT = false;
|
||||
function log(message, color = 'reset') { if (!SILENT) console.log(`${colors[color]}${message}${colors.reset}`); }
|
||||
function logBright(message) { log(message, 'bright'); }
|
||||
function logSuccess(message) { log(`✓ ${message}`, 'green'); }
|
||||
function logWarning(message) { log(`⚠ ${message}`, 'yellow'); }
|
||||
function logError(message) { log(`✗ ${message}`, 'red'); }
|
||||
function logInfo(message) { log(`ℹ ${message}`, 'cyan'); }
|
||||
|
||||
function mapSource(fileSource) {
|
||||
const mapping = {
|
||||
'user': 'user_instruction',
|
||||
'system': 'framework_default',
|
||||
'collaborative': 'user_instruction',
|
||||
'framework': 'framework_default',
|
||||
'automated': 'automated',
|
||||
'migration': 'migration'
|
||||
};
|
||||
return mapping[fileSource] || 'user_instruction';
|
||||
}
|
||||
|
||||
function mapInstructionToRule(instruction) {
|
||||
return {
|
||||
id: instruction.id,
|
||||
text: instruction.text,
|
||||
scope: 'PROJECT_SPECIFIC',
|
||||
applicableProjects: ['*'],
|
||||
quadrant: instruction.quadrant,
|
||||
persistence: instruction.persistence,
|
||||
category: mapCategory(instruction),
|
||||
priority: mapPriority(instruction),
|
||||
temporalScope: instruction.temporal_scope || 'PERMANENT',
|
||||
expiresAt: null,
|
||||
clarityScore: null,
|
||||
specificityScore: null,
|
||||
actionabilityScore: null,
|
||||
validationStatus: 'NOT_VALIDATED',
|
||||
active: instruction.active !== false,
|
||||
source: mapSource(instruction.source || 'user'),
|
||||
createdBy: 'system',
|
||||
createdAt: instruction.timestamp ? new Date(instruction.timestamp) : new Date(),
|
||||
notes: instruction.notes || ''
|
||||
};
|
||||
}
|
||||
|
||||
function mapCategory(instruction) {
|
||||
const text = instruction.text.toLowerCase();
|
||||
const quadrant = instruction.quadrant;
|
||||
|
||||
if (text.includes('security') || text.includes('csp') || text.includes('auth')) return 'security';
|
||||
if (text.includes('privacy') || text.includes('gdpr') || text.includes('consent')) return 'privacy';
|
||||
if (text.includes('values') || text.includes('pluralism') || text.includes('legitimacy')) return 'values';
|
||||
if (quadrant === 'SYSTEM') return 'technical';
|
||||
if (quadrant === 'OPERATIONAL' || quadrant === 'TACTICAL') return 'process';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function mapPriority(instruction) {
|
||||
if (instruction.persistence === 'HIGH') return 80;
|
||||
if (instruction.persistence === 'MEDIUM') return 50;
|
||||
return 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
* @param {Object} options - Sync options
|
||||
* @param {boolean} options.silent - Silent mode (default: false)
|
||||
* @param {boolean} options.dryRun - Dry run mode (default: false)
|
||||
* @param {boolean} options.force - Force sync (default: true when silent)
|
||||
*/
|
||||
async function syncInstructions(options = {}) {
|
||||
// Determine mode: programmatic call or CLI
|
||||
const isDryRun = options.dryRun !== undefined ? options.dryRun : cliDryRun;
|
||||
const isSilent = options.silent !== undefined ? options.silent : cliSilent;
|
||||
const isForce = options.force !== undefined ? options.force : (cliForce || (!isDryRun && isSilent));
|
||||
|
||||
// Set global silent flag for log functions
|
||||
SILENT = isSilent;
|
||||
|
||||
// Track if we created the connection (so we know if we should close it)
|
||||
const wasConnected = mongoose.connection.readyState === 1;
|
||||
|
||||
try {
|
||||
logBright('\n════════════════════════════════════════════════════════════════');
|
||||
logBright(' Tractatus Instruction → Database Sync');
|
||||
logBright('════════════════════════════════════════════════════════════════\n');
|
||||
|
||||
if (isDryRun) logInfo('DRY RUN MODE - No changes will be made\n');
|
||||
|
||||
logInfo('Step 1: Reading instruction file...');
|
||||
if (!fs.existsSync(INSTRUCTION_FILE)) {
|
||||
logError(`Instruction file not found: ${INSTRUCTION_FILE}`);
|
||||
return { success: false, error: 'File not found' };
|
||||
}
|
||||
|
||||
const fileData = JSON.parse(fs.readFileSync(INSTRUCTION_FILE, 'utf8'));
|
||||
const instructions = fileData.instructions || [];
|
||||
|
||||
logSuccess(`Loaded ${instructions.length} instructions from file`);
|
||||
log(` File version: ${fileData.version}`);
|
||||
log(` Last updated: ${fileData.last_updated}\n`);
|
||||
|
||||
logInfo('Step 2: Connecting to MongoDB...');
|
||||
const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
if (!wasConnected) {
|
||||
await mongoose.connect(mongoUri);
|
||||
logSuccess(`Connected to MongoDB: ${mongoUri}\n`);
|
||||
} else {
|
||||
logSuccess(`Using existing MongoDB connection\n`);
|
||||
}
|
||||
|
||||
logInfo('Step 3: Analyzing database state...');
|
||||
const dbRules = await GovernanceRule.find({}).lean();
|
||||
const dbRuleIds = dbRules.map(r => r.id);
|
||||
const fileRuleIds = instructions.map(i => i.id);
|
||||
|
||||
log(` Database has: ${dbRules.length} rules`);
|
||||
log(` File has: ${instructions.length} instructions`);
|
||||
|
||||
const orphanedRules = dbRules.filter(r => !fileRuleIds.includes(r.id));
|
||||
const missingRules = instructions.filter(i => !dbRuleIds.includes(i.id));
|
||||
|
||||
log(` Orphaned (in DB, not in file): ${orphanedRules.length}`);
|
||||
log(` Missing (in file, not in DB): ${missingRules.length}`);
|
||||
log(` Existing (in both): ${instructions.filter(i => dbRuleIds.includes(i.id)).length}\n`);
|
||||
|
||||
if (orphanedRules.length > 0) {
|
||||
logWarning('Orphaned rules found:');
|
||||
orphanedRules.forEach(r => log(` - ${r.id}: "${r.text.substring(0, 60)}..."`, 'yellow'));
|
||||
log('');
|
||||
}
|
||||
|
||||
if (missingRules.length > 0) {
|
||||
logInfo('Missing rules (will be added):');
|
||||
missingRules.forEach(i => log(` + ${i.id}: "${i.text.substring(0, 60)}..."`, 'green'));
|
||||
log('');
|
||||
}
|
||||
|
||||
if (orphanedRules.length > 0 && !isDryRun) {
|
||||
logInfo('Step 4: Handling orphaned rules...');
|
||||
const backupDir = path.dirname(ORPHAN_BACKUP);
|
||||
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
const orphanBackup = {
|
||||
timestamp: new Date().toISOString(),
|
||||
reason: 'Rules found in MongoDB but not in .claude/instruction-history.json',
|
||||
action: 'Soft deleted (marked as inactive)',
|
||||
rules: orphanedRules
|
||||
};
|
||||
|
||||
fs.writeFileSync(ORPHAN_BACKUP, JSON.stringify(orphanBackup, null, 2));
|
||||
logSuccess(`Exported orphaned rules to: ${ORPHAN_BACKUP}`);
|
||||
|
||||
for (const orphan of orphanedRules) {
|
||||
await GovernanceRule.findByIdAndUpdate(orphan._id, {
|
||||
active: false,
|
||||
notes: (orphan.notes || '') + '\n[AUTO-DEACTIVATED: Not found in file-based source of truth on ' + new Date().toISOString() + ']'
|
||||
});
|
||||
}
|
||||
logSuccess(`Deactivated ${orphanedRules.length} orphaned rules\n`);
|
||||
} else if (orphanedRules.length > 0 && isDryRun) {
|
||||
logInfo('Step 4: [DRY RUN] Would deactivate orphaned rules\n');
|
||||
} else {
|
||||
logSuccess('Step 4: No orphaned rules found\n');
|
||||
}
|
||||
|
||||
logInfo('Step 5: Syncing instructions to database...');
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const instruction of instructions) {
|
||||
const ruleData = mapInstructionToRule(instruction);
|
||||
|
||||
if (isDryRun) {
|
||||
if (!dbRuleIds.includes(instruction.id)) {
|
||||
log(` [DRY RUN] Would add: ${instruction.id}`, 'cyan');
|
||||
addedCount++;
|
||||
} else {
|
||||
log(` [DRY RUN] Would update: ${instruction.id}`, 'cyan');
|
||||
updatedCount++;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const existing = await GovernanceRule.findOne({ id: instruction.id });
|
||||
if (existing) {
|
||||
await GovernanceRule.findByIdAndUpdate(existing._id, {
|
||||
...ruleData,
|
||||
clarityScore: existing.clarityScore || ruleData.clarityScore,
|
||||
specificityScore: existing.specificityScore || ruleData.specificityScore,
|
||||
actionabilityScore: existing.actionabilityScore || ruleData.actionabilityScore,
|
||||
lastOptimized: existing.lastOptimized,
|
||||
optimizationHistory: existing.optimizationHistory,
|
||||
validationStatus: existing.validationStatus,
|
||||
lastValidated: existing.lastValidated,
|
||||
validationResults: existing.validationResults,
|
||||
updatedAt: new Date()
|
||||
});
|
||||
updatedCount++;
|
||||
} else {
|
||||
await GovernanceRule.create(ruleData);
|
||||
addedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
logError(` Failed to sync ${instruction.id}: ${error.message}`);
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
log('');
|
||||
logInfo('DRY RUN SUMMARY:');
|
||||
log(` Would add: ${addedCount} rules`);
|
||||
log(` Would update: ${updatedCount} rules`);
|
||||
log(` Would skip: ${skippedCount} rules`);
|
||||
log(` Would deactivate: ${orphanedRules.length} orphaned rules\n`);
|
||||
logInfo('Run with --force to execute changes\n');
|
||||
} else {
|
||||
log('');
|
||||
logSuccess('SYNC COMPLETE:');
|
||||
log(` Added: ${addedCount} rules`, 'green');
|
||||
log(` Updated: ${updatedCount} rules`, 'green');
|
||||
log(` Skipped: ${skippedCount} rules`, 'yellow');
|
||||
log(` Deactivated: ${orphanedRules.length} orphaned rules`, 'yellow');
|
||||
log('');
|
||||
}
|
||||
|
||||
logInfo('Step 6: Verifying final state...');
|
||||
const finalCount = await GovernanceRule.countDocuments({ active: true });
|
||||
const expectedCount = instructions.filter(i => i.active !== false).length;
|
||||
|
||||
if (isDryRun) {
|
||||
log(` Current active rules: ${dbRules.filter(r => r.active).length}`);
|
||||
log(` After sync would be: ${expectedCount}\n`);
|
||||
} else {
|
||||
log(` Active rules in database: ${finalCount}`);
|
||||
log(` Expected from file: ${expectedCount}`);
|
||||
if (finalCount === expectedCount) {
|
||||
logSuccess(' ✓ Counts match!\n');
|
||||
} else {
|
||||
logWarning(` ⚠ Mismatch: ${finalCount} vs ${expectedCount}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Only disconnect if we created the connection
|
||||
if (!wasConnected && mongoose.connection.readyState === 1) {
|
||||
await mongoose.disconnect();
|
||||
logSuccess('Disconnected from MongoDB\n');
|
||||
} else {
|
||||
logSuccess('Leaving connection open for server\n');
|
||||
}
|
||||
|
||||
logBright('════════════════════════════════════════════════════════════════');
|
||||
if (isDryRun) {
|
||||
logInfo('DRY RUN COMPLETE - No changes made');
|
||||
} else {
|
||||
logSuccess('SYNC COMPLETE');
|
||||
}
|
||||
logBright('════════════════════════════════════════════════════════════════\n');
|
||||
|
||||
return { success: true, added: addedCount, updated: updatedCount, skipped: skippedCount, deactivated: orphanedRules.length, finalCount: isDryRun ? null : finalCount };
|
||||
|
||||
} catch (error) {
|
||||
logError(`\nSync failed: ${error.message}`);
|
||||
if (!isSilent) console.error(error.stack);
|
||||
// Only disconnect if we created the connection
|
||||
if (!wasConnected && mongoose.connection.readyState === 1) {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (!cliDryRun && !cliForce && !cliSilent) {
|
||||
console.log('\nUsage:');
|
||||
console.log(' node scripts/sync-instructions-to-db.js --dry-run # Preview changes');
|
||||
console.log(' node scripts/sync-instructions-to-db.js --force # Execute sync');
|
||||
console.log(' node scripts/sync-instructions-to-db.js --silent # Background mode\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
syncInstructions()
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { syncInstructions };
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Sync appropriate files to tractatus-framework public repo
|
||||
|
||||
SRC="/home/theflow/projects/tractatus"
|
||||
DEST="/home/theflow/projects/tractatus-public"
|
||||
|
||||
echo "=== Syncing tractatus -> tractatus-framework (public) ==="
|
||||
|
||||
# Core application files
|
||||
echo "Copying package files..."
|
||||
cp "$SRC/package.json" "$DEST/"
|
||||
cp "$SRC/package-lock.json" "$DEST/"
|
||||
|
||||
# Source code
|
||||
echo "Copying src/ directory..."
|
||||
rsync -av --delete "$SRC/src/" "$DEST/src/"
|
||||
|
||||
# Tests
|
||||
echo "Copying tests/ directory..."
|
||||
rsync -av --delete "$SRC/tests/" "$DEST/tests/"
|
||||
|
||||
# Scripts (excluding sensitive ones)
|
||||
echo "Copying scripts/ directory..."
|
||||
mkdir -p "$DEST/scripts"
|
||||
rsync -av "$SRC/scripts/" "$DEST/scripts/" \
|
||||
--exclude="*backup*" \
|
||||
--exclude="*.old" \
|
||||
--exclude="temp*"
|
||||
|
||||
# Public assets
|
||||
echo "Copying public/ directory..."
|
||||
rsync -av "$SRC/public/" "$DEST/public/" \
|
||||
--exclude="downloads/*.pdf" \
|
||||
--exclude="downloads/*.tar.gz"
|
||||
|
||||
# Systemd service files
|
||||
echo "Copying systemd/ directory..."
|
||||
rsync -av --delete "$SRC/systemd/" "$DEST/systemd/"
|
||||
|
||||
# Deployment quickstart
|
||||
echo "Copying deployment-quickstart/ directory..."
|
||||
rsync -av --delete "$SRC/deployment-quickstart/" "$DEST/deployment-quickstart/"
|
||||
|
||||
# Documentation - markdown
|
||||
echo "Copying docs/markdown/..."
|
||||
mkdir -p "$DEST/docs/markdown"
|
||||
rsync -av --delete "$SRC/docs/markdown/" "$DEST/docs/markdown/"
|
||||
|
||||
# Documentation - API
|
||||
echo "Copying docs/api/..."
|
||||
mkdir -p "$DEST/docs/api"
|
||||
rsync -av --delete "$SRC/docs/api/" "$DEST/docs/api/"
|
||||
|
||||
# Documentation - governance (PUBLIC ONLY)
|
||||
echo "Copying docs/governance/ (public files only)..."
|
||||
mkdir -p "$DEST/docs/governance"
|
||||
cp "$SRC/docs/governance/TRA-VAL-0001-core-values-principles-v1-0.md" "$DEST/docs/governance/" 2>/dev/null || true
|
||||
cp "$SRC/docs/governance/CODING_BEST_PRACTICES_SUMMARY.md" "$DEST/docs/governance/" 2>/dev/null || true
|
||||
|
||||
# Setup instructions
|
||||
echo "Copying setup instructions..."
|
||||
cp "$SRC/SETUP_INSTRUCTIONS.md" "$DEST/"
|
||||
|
||||
# .env.example (but NOT .env)
|
||||
echo "Copying .env.example..."
|
||||
cp "$SRC/.env.example" "$DEST/"
|
||||
|
||||
echo "=== Sync complete ==="
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Production Deployment Test
|
||||
* Initialize BoundaryEnforcer and BlogCuration with MemoryProxy
|
||||
* Verify rule loading and audit trail creation
|
||||
*/
|
||||
|
||||
const BoundaryEnforcer = require('../src/services/BoundaryEnforcer.service');
|
||||
const BlogCuration = require('../src/services/BlogCuration.service');
|
||||
const { getMemoryProxy } = require('../src/services/MemoryProxy.service');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
async function testProductionDeployment() {
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(' Production Deployment Test');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const results = {
|
||||
memoryProxy: { initialized: false },
|
||||
boundaryEnforcer: { initialized: false, rulesLoaded: 0 },
|
||||
blogCuration: { initialized: false, rulesLoaded: 0 },
|
||||
auditTrail: { exists: false, entries: 0 },
|
||||
ruleLoading: { inst_016: false, inst_017: false, inst_018: false }
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Initialize MemoryProxy (shared singleton)
|
||||
console.log('[Step 1] Initializing MemoryProxy...');
|
||||
const memoryProxy = getMemoryProxy();
|
||||
await memoryProxy.initialize();
|
||||
results.memoryProxy.initialized = true;
|
||||
console.log(' ✓ MemoryProxy initialized\n');
|
||||
|
||||
// Step 2: Initialize BoundaryEnforcer
|
||||
console.log('[Step 2] Initializing BoundaryEnforcer...');
|
||||
const enforcerResult = await BoundaryEnforcer.initialize();
|
||||
|
||||
if (enforcerResult.success) {
|
||||
results.boundaryEnforcer.initialized = true;
|
||||
results.boundaryEnforcer.rulesLoaded = enforcerResult.rulesLoaded;
|
||||
console.log(` ✓ BoundaryEnforcer initialized`);
|
||||
console.log(` Rules loaded: ${enforcerResult.rulesLoaded}/3`);
|
||||
console.log(` Rules: ${enforcerResult.enforcementRules.join(', ')}\n`);
|
||||
} else {
|
||||
throw new Error(`BoundaryEnforcer initialization failed: ${enforcerResult.error}`);
|
||||
}
|
||||
|
||||
// Step 3: Initialize BlogCuration
|
||||
console.log('[Step 3] Initializing BlogCuration...');
|
||||
const blogResult = await BlogCuration.initialize();
|
||||
|
||||
if (blogResult.success) {
|
||||
results.blogCuration.initialized = true;
|
||||
results.blogCuration.rulesLoaded = blogResult.rulesLoaded;
|
||||
console.log(` ✓ BlogCuration initialized`);
|
||||
console.log(` Rules loaded: ${blogResult.rulesLoaded}/3`);
|
||||
console.log(` Rules: ${blogResult.enforcementRules.join(', ')}\n`);
|
||||
} else {
|
||||
throw new Error(`BlogCuration initialization failed: ${blogResult.error}`);
|
||||
}
|
||||
|
||||
// Step 4: Test rule loading from memory
|
||||
console.log('[Step 4] Verifying rule loading from .memory/...');
|
||||
const criticalRules = ['inst_016', 'inst_017', 'inst_018'];
|
||||
|
||||
for (const ruleId of criticalRules) {
|
||||
const rule = await memoryProxy.getRule(ruleId);
|
||||
if (rule) {
|
||||
results.ruleLoading[ruleId] = true;
|
||||
console.log(` ✓ ${ruleId}: ${rule.text.substring(0, 60)}...`);
|
||||
} else {
|
||||
console.log(` ✗ ${ruleId}: NOT FOUND`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Step 5: Test enforcement with audit logging
|
||||
console.log('[Step 5] Testing enforcement with audit trail...');
|
||||
|
||||
const testAction = {
|
||||
description: 'Production deployment test - technical implementation',
|
||||
domain: 'technical',
|
||||
type: 'deployment_test'
|
||||
};
|
||||
|
||||
const enforcementResult = BoundaryEnforcer.enforce(testAction, {
|
||||
sessionId: 'production-deployment-test'
|
||||
});
|
||||
|
||||
console.log(` ✓ Enforcement test: ${enforcementResult.allowed ? 'ALLOWED' : 'BLOCKED'}`);
|
||||
console.log(` Domain: ${enforcementResult.domain}`);
|
||||
console.log(` Human required: ${enforcementResult.humanRequired ? 'Yes' : 'No'}\n`);
|
||||
|
||||
// Step 6: Verify audit trail creation
|
||||
console.log('[Step 6] Verifying audit trail...');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const auditPath = path.join(__dirname, '../.memory/audit', `decisions-${today}.jsonl`);
|
||||
|
||||
try {
|
||||
const auditData = await fs.readFile(auditPath, 'utf8');
|
||||
const auditLines = auditData.trim().split('\n');
|
||||
results.auditTrail.exists = true;
|
||||
results.auditTrail.entries = auditLines.length;
|
||||
|
||||
console.log(` ✓ Audit trail exists: ${auditPath}`);
|
||||
console.log(` Entries: ${auditLines.length}`);
|
||||
|
||||
// Show last entry
|
||||
if (auditLines.length > 0) {
|
||||
const lastEntry = JSON.parse(auditLines[auditLines.length - 1]);
|
||||
console.log(`\n Last entry:`);
|
||||
console.log(` Session: ${lastEntry.sessionId}`);
|
||||
console.log(` Action: ${lastEntry.action}`);
|
||||
console.log(` Allowed: ${lastEntry.allowed}`);
|
||||
console.log(` Rules checked: ${lastEntry.rulesChecked.join(', ')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ✗ Audit trail not found: ${error.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n✗ Deployment test failed: ${error.message}\n`);
|
||||
if (error.stack) {
|
||||
console.error('Stack trace:', error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Results summary
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log(' DEPLOYMENT TEST RESULTS');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
console.log('✅ PRODUCTION DEPLOYMENT SUCCESSFUL\n');
|
||||
|
||||
console.log('Services Initialized:');
|
||||
console.log(` • MemoryProxy: ${results.memoryProxy.initialized ? '✅' : '❌'}`);
|
||||
console.log(` • BoundaryEnforcer: ${results.boundaryEnforcer.initialized ? '✅' : '❌'} (${results.boundaryEnforcer.rulesLoaded}/3 rules)`);
|
||||
console.log(` • BlogCuration: ${results.blogCuration.initialized ? '✅' : '❌'} (${results.blogCuration.rulesLoaded}/3 rules)`);
|
||||
|
||||
console.log('\nCritical Rules Loaded:');
|
||||
console.log(` • inst_016: ${results.ruleLoading.inst_016 ? '✅' : '❌'} (No fabricated statistics)`);
|
||||
console.log(` • inst_017: ${results.ruleLoading.inst_017 ? '✅' : '❌'} (No absolute guarantees)`);
|
||||
console.log(` • inst_018: ${results.ruleLoading.inst_018 ? '✅' : '❌'} (Accurate status claims)`);
|
||||
|
||||
console.log('\nAudit Trail:');
|
||||
console.log(` • Created: ${results.auditTrail.exists ? '✅' : '❌'}`);
|
||||
console.log(` • Entries: ${results.auditTrail.entries}`);
|
||||
|
||||
console.log('\n📊 Framework Status: 🟢 OPERATIONAL');
|
||||
console.log('\nNext Steps:');
|
||||
console.log(' 1. ✅ Services initialized with MemoryProxy');
|
||||
console.log(' 2. ✅ Rules loaded from .memory/governance/');
|
||||
console.log(' 3. ✅ Audit trail active in .memory/audit/');
|
||||
console.log(' 4. 🔄 Monitor audit logs for insights');
|
||||
console.log(' 5. 🔄 Integrate remaining services (Classifier, Validator, Verifier)');
|
||||
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
}
|
||||
|
||||
// Run test
|
||||
testProductionDeployment();
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Action Pattern Tracker
|
||||
*
|
||||
* Tracks patterns indicating "stuck in loop" situations:
|
||||
* - Same file edited multiple times consecutively
|
||||
* - Same function/section modified repeatedly
|
||||
* - User frustration signals in messages
|
||||
*
|
||||
* This feeds into:
|
||||
* - MetacognitiveVerifier (triggers verification on 3+ failed attempts)
|
||||
* - ContextPressureMonitor (elevates pressure on loop detection)
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/track-action-patterns.js --action edit --file path/to/file
|
||||
* node scripts/track-action-patterns.js --check
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SESSION_STATE_PATH = path.join(__dirname, '../.claude/session-state.json');
|
||||
const ACTION_PATTERNS_PATH = path.join(__dirname, '../.claude/action-patterns.json');
|
||||
|
||||
/**
|
||||
* Load session state
|
||||
*/
|
||||
function loadSessionState() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
||||
} catch (err) {
|
||||
console.error(`Error loading session state: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load action patterns (or initialize)
|
||||
*/
|
||||
function loadActionPatterns() {
|
||||
try {
|
||||
if (fs.existsSync(ACTION_PATTERNS_PATH)) {
|
||||
return JSON.parse(fs.readFileSync(ACTION_PATTERNS_PATH, 'utf8'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Warning: Could not load action patterns: ${err.message}`);
|
||||
}
|
||||
|
||||
// Initialize new patterns structure
|
||||
return {
|
||||
version: "1.0",
|
||||
session_id: null,
|
||||
started: new Date().toISOString(),
|
||||
actions: [],
|
||||
patterns: {
|
||||
repeated_edits: [],
|
||||
same_file_edits: {},
|
||||
user_frustration_signals: []
|
||||
},
|
||||
alerts: [],
|
||||
last_updated: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save action patterns
|
||||
*/
|
||||
function saveActionPatterns(patterns) {
|
||||
patterns.last_updated = new Date().toISOString();
|
||||
fs.writeFileSync(ACTION_PATTERNS_PATH, JSON.stringify(patterns, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Track new action
|
||||
*/
|
||||
function trackAction(actionType, filePath) {
|
||||
const patterns = loadActionPatterns();
|
||||
const sessionState = loadSessionState();
|
||||
|
||||
// Update session ID if changed
|
||||
if (patterns.session_id !== sessionState.session_id) {
|
||||
patterns.session_id = sessionState.session_id;
|
||||
patterns.actions = []; // Reset for new session
|
||||
patterns.patterns.same_file_edits = {};
|
||||
patterns.patterns.repeated_edits = [];
|
||||
patterns.alerts = [];
|
||||
}
|
||||
|
||||
// Add action
|
||||
const action = {
|
||||
type: actionType,
|
||||
file: filePath,
|
||||
timestamp: new Date().toISOString(),
|
||||
message_number: sessionState.message_count
|
||||
};
|
||||
patterns.actions.push(action);
|
||||
|
||||
// Track same-file edits
|
||||
if (actionType === 'edit' || actionType === 'write') {
|
||||
if (!patterns.patterns.same_file_edits[filePath]) {
|
||||
patterns.patterns.same_file_edits[filePath] = {
|
||||
count: 0,
|
||||
timestamps: [],
|
||||
alert_threshold: 3
|
||||
};
|
||||
}
|
||||
patterns.patterns.same_file_edits[filePath].count++;
|
||||
patterns.patterns.same_file_edits[filePath].timestamps.push(action.timestamp);
|
||||
|
||||
// Check for loop pattern
|
||||
if (patterns.patterns.same_file_edits[filePath].count >= 3) {
|
||||
// Check if edits are recent (within last 10 actions)
|
||||
const recentActions = patterns.actions.slice(-10);
|
||||
const recentEditsOfThisFile = recentActions.filter(a =>
|
||||
(a.type === 'edit' || a.type === 'write') && a.file === filePath
|
||||
);
|
||||
|
||||
if (recentEditsOfThisFile.length >= 3) {
|
||||
const alert = {
|
||||
type: 'repeated_file_edit',
|
||||
severity: 'MEDIUM',
|
||||
file: filePath,
|
||||
count: patterns.patterns.same_file_edits[filePath].count,
|
||||
message: `File edited ${patterns.patterns.same_file_edits[filePath].count} times - possible stuck loop`,
|
||||
timestamp: new Date().toISOString(),
|
||||
recommendation: 'Run MetacognitiveVerifier to assess current approach'
|
||||
};
|
||||
patterns.alerts.push(alert);
|
||||
|
||||
console.log(`⚠️ ALERT: Repeated edits to ${path.basename(filePath)}`);
|
||||
console.log(` Count: ${patterns.patterns.same_file_edits[filePath].count}`);
|
||||
console.log(` Recommendation: Pause and verify approach`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only last 100 actions
|
||||
if (patterns.actions.length > 100) {
|
||||
patterns.actions = patterns.actions.slice(-100);
|
||||
}
|
||||
|
||||
saveActionPatterns(patterns);
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for active patterns/alerts
|
||||
*/
|
||||
function checkPatterns() {
|
||||
const patterns = loadActionPatterns();
|
||||
const activeAlerts = patterns.alerts.filter(alert => {
|
||||
// Consider alerts from last hour as active
|
||||
const alertTime = new Date(alert.timestamp);
|
||||
const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
||||
return alertTime > hourAgo;
|
||||
});
|
||||
|
||||
if (activeAlerts.length === 0) {
|
||||
console.log('✅ No active pattern alerts');
|
||||
return { hasAlerts: false, alerts: [] };
|
||||
}
|
||||
|
||||
console.log(`⚠️ ${activeAlerts.length} active alert(s):`);
|
||||
activeAlerts.forEach((alert, i) => {
|
||||
console.log(`\n${i + 1}. [${alert.severity}] ${alert.type}`);
|
||||
console.log(` ${alert.message}`);
|
||||
console.log(` Recommendation: ${alert.recommendation}`);
|
||||
});
|
||||
|
||||
return { hasAlerts: true, alerts: activeAlerts };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pattern summary
|
||||
*/
|
||||
function getPatternSummary() {
|
||||
const patterns = loadActionPatterns();
|
||||
|
||||
const summary = {
|
||||
total_actions: patterns.actions.length,
|
||||
files_edited_multiple_times: Object.keys(patterns.patterns.same_file_edits).filter(
|
||||
file => patterns.patterns.same_file_edits[file].count >= 2
|
||||
),
|
||||
active_alerts: patterns.alerts.filter(alert => {
|
||||
const alertTime = new Date(alert.timestamp);
|
||||
const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
||||
return alertTime > hourAgo;
|
||||
}).length
|
||||
};
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes('--check')) {
|
||||
checkPatterns();
|
||||
} else if (args.includes('--summary')) {
|
||||
const summary = getPatternSummary();
|
||||
console.log('Action Pattern Summary:');
|
||||
console.log(` Total actions: ${summary.total_actions}`);
|
||||
console.log(` Files with multiple edits: ${summary.files_edited_multiple_times.length}`);
|
||||
if (summary.files_edited_multiple_times.length > 0) {
|
||||
summary.files_edited_multiple_times.forEach(file => {
|
||||
console.log(` - ${path.basename(file)}`);
|
||||
});
|
||||
}
|
||||
console.log(` Active alerts: ${summary.active_alerts}`);
|
||||
} else if (args.includes('--action')) {
|
||||
const actionIndex = args.indexOf('--action');
|
||||
const fileIndex = args.indexOf('--file');
|
||||
|
||||
if (actionIndex === -1 || fileIndex === -1) {
|
||||
console.error('Usage: --action <type> --file <path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const actionType = args[actionIndex + 1];
|
||||
const filePath = args[fileIndex + 1];
|
||||
|
||||
trackAction(actionType, filePath);
|
||||
console.log(`✅ Tracked: ${actionType} on ${path.basename(filePath)}`);
|
||||
} else {
|
||||
console.log('Usage:');
|
||||
console.log(' --action <type> --file <path> Track an action');
|
||||
console.log(' --check Check for active alerts');
|
||||
console.log(' --summary Show pattern summary');
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* User Suggestion Tracker
|
||||
*
|
||||
* Tracks user technical hypotheses and debugging suggestions so that:
|
||||
* - MetacognitiveVerifier can check if user hypothesis was tested
|
||||
* - BoundaryEnforcer can flag ignoring user expertise
|
||||
* - CrossReferenceValidator can match actions against suggestions
|
||||
*
|
||||
* Implements inst_049: "Test user hypothesis first"
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/track-user-suggestions.js --add "user hypothesis text"
|
||||
* node scripts/track-user-suggestions.js --mark-tested "hypothesis id"
|
||||
* node scripts/track-user-suggestions.js --check-untested
|
||||
*
|
||||
* Copyright 2025 Tractatus Project
|
||||
* Licensed under Apache License 2.0
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SUGGESTIONS_PATH = path.join(__dirname, '../.claude/user-suggestions.json');
|
||||
const SESSION_STATE_PATH = path.join(__dirname, '../.claude/session-state.json');
|
||||
|
||||
/**
|
||||
* Load user suggestions
|
||||
*/
|
||||
function loadSuggestions() {
|
||||
try {
|
||||
if (fs.existsSync(SUGGESTIONS_PATH)) {
|
||||
return JSON.parse(fs.readFileSync(SUGGESTIONS_PATH, 'utf8'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Warning: Could not load suggestions: ${err.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
version: "1.0",
|
||||
session_id: null,
|
||||
suggestions: [],
|
||||
last_updated: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user suggestions
|
||||
*/
|
||||
function saveSuggestions(data) {
|
||||
data.last_updated = new Date().toISOString();
|
||||
fs.writeFileSync(SUGGESTIONS_PATH, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session ID
|
||||
*/
|
||||
function getCurrentSessionId() {
|
||||
try {
|
||||
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
||||
return sessionState.session_id;
|
||||
} catch (err) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new user suggestion/hypothesis
|
||||
*/
|
||||
function addSuggestion(text) {
|
||||
const data = loadSuggestions();
|
||||
const sessionId = getCurrentSessionId();
|
||||
|
||||
// Reset if new session
|
||||
if (data.session_id !== sessionId) {
|
||||
data.session_id = sessionId;
|
||||
data.suggestions = [];
|
||||
}
|
||||
|
||||
// Extract key phrases that indicate technical hypothesis
|
||||
const hypothesisIndicators = [
|
||||
'could be',
|
||||
'might be',
|
||||
'issue',
|
||||
'problem',
|
||||
'try',
|
||||
'check',
|
||||
'examine',
|
||||
'look at',
|
||||
'debug',
|
||||
'test'
|
||||
];
|
||||
|
||||
const isHypothesis = hypothesisIndicators.some(indicator =>
|
||||
text.toLowerCase().includes(indicator)
|
||||
);
|
||||
|
||||
const suggestion = {
|
||||
id: `sugg_${Date.now()}`,
|
||||
text: text,
|
||||
timestamp: new Date().toISOString(),
|
||||
tested: false,
|
||||
result: null,
|
||||
is_hypothesis: isHypothesis,
|
||||
priority: isHypothesis ? 'HIGH' : 'MEDIUM'
|
||||
};
|
||||
|
||||
data.suggestions.push(suggestion);
|
||||
saveSuggestions(data);
|
||||
|
||||
console.log(`✅ Tracked user suggestion: ${suggestion.id}`);
|
||||
if (isHypothesis) {
|
||||
console.log(` 📋 Marked as HYPOTHESIS - should test before alternatives`);
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark suggestion as tested
|
||||
*/
|
||||
function markTested(suggestionId, result = null) {
|
||||
const data = loadSuggestions();
|
||||
const suggestion = data.suggestions.find(s => s.id === suggestionId);
|
||||
|
||||
if (!suggestion) {
|
||||
console.error(`Error: Suggestion ${suggestionId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
suggestion.tested = true;
|
||||
suggestion.result = result;
|
||||
suggestion.tested_at = new Date().toISOString();
|
||||
|
||||
saveSuggestions(data);
|
||||
|
||||
console.log(`✅ Marked suggestion as tested: ${suggestionId}`);
|
||||
if (result) {
|
||||
console.log(` Result: ${result}`);
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for untested hypotheses
|
||||
*/
|
||||
function checkUntested() {
|
||||
const data = loadSuggestions();
|
||||
const untested = data.suggestions.filter(s => !s.tested && s.is_hypothesis);
|
||||
|
||||
if (untested.length === 0) {
|
||||
console.log('✅ All user hypotheses have been tested');
|
||||
return { hasUntested: false, untested: [] };
|
||||
}
|
||||
|
||||
console.log(`⚠️ ${untested.length} untested user hypothesis(es):`);
|
||||
untested.forEach((sugg, i) => {
|
||||
console.log(`\n${i + 1}. [${sugg.priority}] ${sugg.id}`);
|
||||
console.log(` "${sugg.text}"`);
|
||||
console.log(` Suggested: ${new Date(sugg.timestamp).toLocaleString()}`);
|
||||
});
|
||||
|
||||
console.log('\n💡 inst_049: Test user hypotheses BEFORE pursuing alternatives');
|
||||
|
||||
return { hasUntested: true, untested };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggestion summary
|
||||
*/
|
||||
function getSummary() {
|
||||
const data = loadSuggestions();
|
||||
|
||||
const summary = {
|
||||
total: data.suggestions.length,
|
||||
hypotheses: data.suggestions.filter(s => s.is_hypothesis).length,
|
||||
tested: data.suggestions.filter(s => s.tested).length,
|
||||
untested: data.suggestions.filter(s => !s.tested).length,
|
||||
untested_hypotheses: data.suggestions.filter(s => !s.tested && s.is_hypothesis).length
|
||||
};
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display summary
|
||||
*/
|
||||
function displaySummary() {
|
||||
const summary = getSummary();
|
||||
|
||||
console.log('User Suggestion Summary:');
|
||||
console.log(` Total suggestions: ${summary.total}`);
|
||||
console.log(` Hypotheses: ${summary.hypotheses}`);
|
||||
console.log(` Tested: ${summary.tested}`);
|
||||
console.log(` Untested: ${summary.untested}`);
|
||||
|
||||
if (summary.untested_hypotheses > 0) {
|
||||
console.log(`\n ⚠️ ${summary.untested_hypotheses} untested hypothesis(es) - violation risk`);
|
||||
} else {
|
||||
console.log(`\n ✅ All hypotheses tested`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help')) {
|
||||
console.log('User Suggestion Tracker');
|
||||
console.log('\nUsage:');
|
||||
console.log(' --add "text" Add user suggestion/hypothesis');
|
||||
console.log(' --mark-tested ID [result] Mark suggestion as tested');
|
||||
console.log(' --check-untested Check for untested hypotheses');
|
||||
console.log(' --summary Show summary statistics');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args.includes('--add')) {
|
||||
const index = args.indexOf('--add');
|
||||
const text = args[index + 1];
|
||||
if (!text) {
|
||||
console.error('Error: --add requires suggestion text');
|
||||
process.exit(1);
|
||||
}
|
||||
addSuggestion(text);
|
||||
} else if (args.includes('--mark-tested')) {
|
||||
const index = args.indexOf('--mark-tested');
|
||||
const id = args[index + 1];
|
||||
const result = args[index + 2] || null;
|
||||
if (!id) {
|
||||
console.error('Error: --mark-tested requires suggestion ID');
|
||||
process.exit(1);
|
||||
}
|
||||
markTested(id, result);
|
||||
} else if (args.includes('--check-untested')) {
|
||||
const result = checkUntested();
|
||||
process.exit(result.hasUntested ? 1 : 0);
|
||||
} else if (args.includes('--summary')) {
|
||||
displaySummary();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Update Cache Version - Unified Cache Busting
|
||||
*
|
||||
* Updates all HTML files with a consistent cache-busting version string.
|
||||
* Format: v={package.version}.{timestamp}
|
||||
* Example: v=0.1.0.1760201234
|
||||
*
|
||||
* This ensures all assets (CSS, JS) are loaded with the same version,
|
||||
* solving the inconsistent cache busting problem.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
// Generate cache version: package version + timestamp
|
||||
const CACHE_VERSION = `${packageJson.version}.${Date.now()}`;
|
||||
|
||||
// HTML files to update (relative to project root)
|
||||
const HTML_FILES = [
|
||||
'public/index.html',
|
||||
'public/docs.html',
|
||||
'public/faq.html',
|
||||
'public/researcher.html',
|
||||
'public/implementer.html',
|
||||
'public/leader.html',
|
||||
'public/about.html',
|
||||
'public/privacy.html',
|
||||
'public/blog.html',
|
||||
'public/blog-post.html',
|
||||
'public/docs-viewer.html',
|
||||
'public/api-reference.html',
|
||||
'public/media-inquiry.html',
|
||||
'public/case-submission.html',
|
||||
'public/koha.html',
|
||||
'public/check-version.html'
|
||||
];
|
||||
|
||||
/**
|
||||
* Update cache version in a file
|
||||
* Replaces all instances of ?v=X with ?v={CACHE_VERSION}
|
||||
*/
|
||||
function updateCacheVersion(filePath) {
|
||||
try {
|
||||
const fullPath = path.join(__dirname, '..', filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.warn(`⚠️ File not found: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
const originalContent = content;
|
||||
|
||||
// Pattern: ?v=ANYTHING → ?v={CACHE_VERSION}
|
||||
// Matches: ?v=1.0.4, ?v=1759833751, ?v=1.0.5.1760123456
|
||||
content = content.replace(/\?v=[0-9a-zA-Z._-]+/g, `?v=${CACHE_VERSION}`);
|
||||
|
||||
// Only write if changed
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
|
||||
// Count replacements
|
||||
const matches = originalContent.match(/\?v=[0-9a-zA-Z._-]+/g) || [];
|
||||
console.log(`✅ ${filePath}: Updated ${matches.length} cache version(s)`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`ℹ️ ${filePath}: No changes needed`);
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error updating ${filePath}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
function main() {
|
||||
console.log('');
|
||||
console.log('═'.repeat(70));
|
||||
console.log(' Tractatus - Cache Version Update');
|
||||
console.log('═'.repeat(70));
|
||||
console.log('');
|
||||
console.log(`📦 Package version: ${packageJson.version}`);
|
||||
console.log(`🔄 New cache version: ${CACHE_VERSION}`);
|
||||
console.log('');
|
||||
|
||||
let updatedCount = 0;
|
||||
let totalFiles = 0;
|
||||
|
||||
HTML_FILES.forEach(file => {
|
||||
totalFiles++;
|
||||
if (updateCacheVersion(file)) {
|
||||
updatedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('═'.repeat(70));
|
||||
console.log(` Summary: ${updatedCount}/${totalFiles} files updated`);
|
||||
console.log('═'.repeat(70));
|
||||
console.log('');
|
||||
|
||||
if (updatedCount > 0) {
|
||||
console.log('✅ Cache version updated successfully!');
|
||||
console.log(` All assets will now use: ?v=${CACHE_VERSION}`);
|
||||
} else {
|
||||
console.log('ℹ️ No files needed updating');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { updateCacheVersion, CACHE_VERSION };
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
/**
|
||||
* Update Core Concepts Document for Phase 5
|
||||
* Updates service count, adds MongoDB, API Memory, BlogCuration service
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
const marked = require('marked');
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
const UPDATES = {
|
||||
// Update overview section
|
||||
oldOverview: 'The Tractatus framework consists of five interconnected services that work together to ensure AI operations remain within safe boundaries. Each service addresses a specific aspect of AI safety.',
|
||||
|
||||
newOverview: `The Tractatus framework consists of six interconnected services that work together to ensure AI operations remain within safe boundaries. Each service addresses a specific aspect of AI safety.
|
||||
|
||||
**Current Status**: Production-ready (Phase 5 complete). All services integrated with MongoDB persistence and optional Anthropic API Memory enhancement.
|
||||
|
||||
**Architecture**: Hybrid memory system combining MongoDB (required persistent storage), Anthropic API Memory (optional session enhancement), and filesystem audit trails (debug logging). See the Architectural Overview document for complete technical details.`,
|
||||
|
||||
// Add MongoDB persistence section after service 5
|
||||
mongodbSection: `
|
||||
|
||||
## 6. BlogCuration
|
||||
|
||||
### Purpose
|
||||
|
||||
Validates blog content and social media posts against inst_016-018 governance rules to prevent fabricated statistics, absolute guarantees, and unverified claims.
|
||||
|
||||
### The Problem It Solves
|
||||
|
||||
Marketing content can inadvertently include:
|
||||
- Fabricated statistics without sources ("95% of users report...")
|
||||
- Absolute guarantees ("guaranteed 100% secure")
|
||||
- Unverified customer claims ("thousands of happy customers")
|
||||
|
||||
Without validation, these violations damage credibility and can constitute false advertising.
|
||||
|
||||
### How It Works
|
||||
|
||||
**Validation Process:**
|
||||
|
||||
1. **Rule Loading**: Loads inst_016, inst_017, inst_018 from MongoDB
|
||||
2. **Content Analysis**: Scans text for violation patterns
|
||||
3. **Violation Detection**: Identifies specific rule violations
|
||||
4. **Blocking**: Prevents publication if violations found
|
||||
5. **Audit Trail**: Logs all validation attempts to MongoDB
|
||||
|
||||
**Enforced Rules:**
|
||||
|
||||
- **inst_016**: No fabricated statistics without validation evidence
|
||||
- **inst_017**: No absolute guarantees about capabilities
|
||||
- **inst_018**: No unverified claims about users/customers
|
||||
|
||||
### Example Validation
|
||||
|
||||
\`\`\`javascript
|
||||
const BlogCuration = require('./services/BlogCuration.service');
|
||||
|
||||
const blogPost = {
|
||||
title: "Why Choose Tractatus",
|
||||
content: "Join thousands of satisfied customers using our framework!"
|
||||
};
|
||||
|
||||
const validation = await BlogCuration.validateContent(blogPost.content);
|
||||
|
||||
if (!validation.allowed) {
|
||||
console.log('Violation:', validation.violations[0]);
|
||||
// Output: "inst_018: Unverified claim about 'thousands of satisfied customers'"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Integration
|
||||
|
||||
- **MongoDB**: Loads governance rules, stores validation logs
|
||||
- **BoundaryEnforcer**: Shares inst_016-018 enforcement logic
|
||||
- **Audit Trail**: All validations logged to \`.memory/audit/decisions-{date}.jsonl\`
|
||||
|
||||
---
|
||||
|
||||
## MongoDB Persistence Architecture
|
||||
|
||||
**Phase 5 Achievement**: All services now persist to MongoDB for production reliability.
|
||||
|
||||
### Collections
|
||||
|
||||
1. **governanceRules**: 18 active instructions (inst_001 through inst_019)
|
||||
2. **auditLogs**: Decision audit trail with full context
|
||||
3. **sessionState**: Current session state and token tracking
|
||||
4. **verificationLogs**: MetacognitiveVerifier confidence scores and decisions
|
||||
5. **documents**: Framework documentation (this document)
|
||||
|
||||
### Benefits Over Filesystem
|
||||
|
||||
- **Fast indexed queries** by rule ID, quadrant, persistence level
|
||||
- **Atomic updates** (no race conditions)
|
||||
- **Aggregation for analytics** (violation patterns, usage stats)
|
||||
- **Built-in replication** and backup
|
||||
- **Transaction support** for multi-document operations
|
||||
|
||||
### API Memory Integration (Optional)
|
||||
|
||||
**Anthropic API Memory** provides session continuity but does NOT replace MongoDB:
|
||||
|
||||
- **MongoDB**: Required for persistent storage, production systems
|
||||
- **API Memory**: Optional enhancement for conversation context
|
||||
- **Architecture**: Hybrid system with graceful degradation
|
||||
|
||||
If API Memory is unavailable, all services continue functioning with MongoDB alone.
|
||||
|
||||
### Environment Setup
|
||||
|
||||
\`\`\`bash
|
||||
# Required
|
||||
MONGODB_URI=mongodb://localhost:27017/tractatus_dev
|
||||
MONGODB_DB=tractatus_dev
|
||||
|
||||
# Optional (enables API Memory features)
|
||||
CLAUDE_API_KEY=your_api_key_here
|
||||
\`\`\`
|
||||
|
||||
See Implementation Guide for complete setup instructions.`,
|
||||
|
||||
// Update "How Services Work Together" section
|
||||
oldWorkTogether: 'These five services form a complete governance framework',
|
||||
newWorkTogether: 'These six services form a complete governance framework'
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log('=== Updating Core Concepts Document ===\n');
|
||||
|
||||
let client;
|
||||
|
||||
try {
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Fetch current document
|
||||
const doc = await collection.findOne({ slug: 'core-concepts-of-the-tractatus-framework' });
|
||||
|
||||
if (!doc) {
|
||||
throw new Error('Core Concepts document not found');
|
||||
}
|
||||
|
||||
console.log('Current document loaded');
|
||||
console.log(`Current length: ${doc.content_markdown.length} characters\n`);
|
||||
|
||||
// Apply updates
|
||||
let updated = doc.content_markdown;
|
||||
|
||||
// Update overview
|
||||
updated = updated.replace(UPDATES.oldOverview, UPDATES.newOverview);
|
||||
console.log('✓ Updated overview section');
|
||||
|
||||
// Add BlogCuration and MongoDB sections before "How the Services Work Together"
|
||||
const insertionPoint = updated.indexOf('## How the Services Work Together');
|
||||
if (insertionPoint > -1) {
|
||||
updated = updated.slice(0, insertionPoint) + UPDATES.mongodbSection + '\n\n' + updated.slice(insertionPoint);
|
||||
console.log('✓ Added BlogCuration service section');
|
||||
console.log('✓ Added MongoDB Persistence Architecture section');
|
||||
} else {
|
||||
console.log('⚠ Could not find insertion point for BlogCuration section');
|
||||
}
|
||||
|
||||
// Update "How the Services Work Together"
|
||||
updated = updated.replace('These five services form a complete governance framework', 'These six services form a complete governance framework');
|
||||
console.log('✓ Updated service count in integration section');
|
||||
|
||||
console.log(`\nNew length: ${updated.length} characters`);
|
||||
console.log(`Change: +${updated.length - doc.content_markdown.length} characters\n`);
|
||||
|
||||
// Regenerate HTML
|
||||
const content_html = marked.parse(updated);
|
||||
|
||||
// Update document
|
||||
const result = await collection.updateOne(
|
||||
{ slug: 'core-concepts-of-the-tractatus-framework' },
|
||||
{
|
||||
$set: {
|
||||
content_markdown: updated,
|
||||
content_html: content_html,
|
||||
'metadata.date_updated': new Date(),
|
||||
'metadata.version': '1.1'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
console.log('✓ Document updated in MongoDB');
|
||||
console.log('✓ Version updated to 1.1');
|
||||
console.log('\n=== Update Complete ===');
|
||||
} else {
|
||||
console.log('⚠ No changes made');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
/**
|
||||
* Update Document Metadata and Ordering
|
||||
* Sets proper order, category, and audience for public documents
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
// MongoDB connection
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
// Document metadata updates
|
||||
const DOCUMENT_UPDATES = [
|
||||
// GETTING STARTED (order: 1-3)
|
||||
{
|
||||
slug: 'architectural-overview-and-research-status',
|
||||
order: 1,
|
||||
category: 'reference',
|
||||
audience: 'general',
|
||||
visibility: 'public'
|
||||
},
|
||||
{
|
||||
slug: 'core-concepts-of-the-tractatus-framework',
|
||||
order: 2,
|
||||
category: 'conceptual',
|
||||
audience: 'general',
|
||||
visibility: 'public'
|
||||
},
|
||||
{
|
||||
slug: 'implementation-guide',
|
||||
order: 3,
|
||||
category: 'practical',
|
||||
audience: 'technical',
|
||||
visibility: 'public'
|
||||
},
|
||||
// FRAMEWORK DETAILS (order: 4-6)
|
||||
{
|
||||
slug: 'tractatus-ai-safety-framework-core-values-and-principles',
|
||||
order: 4,
|
||||
category: 'conceptual',
|
||||
audience: 'general',
|
||||
visibility: 'public'
|
||||
},
|
||||
{
|
||||
slug: 'case-studies-real-world-llm-failure-modes',
|
||||
order: 5,
|
||||
category: 'practical',
|
||||
audience: 'general',
|
||||
visibility: 'public'
|
||||
},
|
||||
{
|
||||
slug: 'ai-governance-business-case-template-tractatus-framework',
|
||||
order: 6,
|
||||
category: 'practical',
|
||||
audience: 'business',
|
||||
visibility: 'public'
|
||||
},
|
||||
// REFERENCE (order: 7)
|
||||
{
|
||||
slug: 'tractatus-agentic-governance-system-glossary-of-terms',
|
||||
order: 7,
|
||||
category: 'reference',
|
||||
audience: 'general',
|
||||
visibility: 'public'
|
||||
}
|
||||
];
|
||||
|
||||
async function main() {
|
||||
console.log('=== Updating Document Metadata ===\n');
|
||||
|
||||
let client;
|
||||
|
||||
try {
|
||||
// Connect to MongoDB
|
||||
console.log('Connecting to MongoDB...');
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
console.log('✓ Connected\n');
|
||||
|
||||
let updated = 0;
|
||||
let notFound = 0;
|
||||
|
||||
// Update each document
|
||||
for (const doc of DOCUMENT_UPDATES) {
|
||||
console.log(`Updating: ${doc.slug}`);
|
||||
console.log(` Order: ${doc.order} | Category: ${doc.category} | Audience: ${doc.audience}`);
|
||||
|
||||
const result = await collection.updateOne(
|
||||
{ slug: doc.slug },
|
||||
{
|
||||
$set: {
|
||||
order: doc.order,
|
||||
category: doc.category,
|
||||
audience: doc.audience,
|
||||
visibility: doc.visibility
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount > 0) {
|
||||
console.log(` ✓ Updated\n`);
|
||||
updated++;
|
||||
} else {
|
||||
console.log(` ⚠ Not found in database\n`);
|
||||
notFound++;
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('=== Summary ===\n');
|
||||
console.log(`✓ Updated: ${updated} documents`);
|
||||
if (notFound > 0) {
|
||||
console.log(`⚠ Not found: ${notFound} documents`);
|
||||
}
|
||||
console.log(`\nTotal processed: ${DOCUMENT_UPDATES.length}`);
|
||||
|
||||
// Verify organization
|
||||
console.log('\n=== Verification ===\n');
|
||||
const publicDocs = await collection.find({ visibility: 'public' })
|
||||
.sort({ order: 1 })
|
||||
.project({ title: 1, order: 1, category: 1, audience: 1 })
|
||||
.toArray();
|
||||
|
||||
console.log('Public documents (sorted by order):');
|
||||
publicDocs.forEach(doc => {
|
||||
console.log(` ${doc.order}. ${doc.title}`);
|
||||
console.log(` Category: ${doc.category} | Audience: ${doc.audience}\n`);
|
||||
});
|
||||
|
||||
const archivedCount = await collection.countDocuments({ visibility: 'archived' });
|
||||
console.log(`\n📦 Archived: ${archivedCount} documents`);
|
||||
console.log(`📖 Public: ${publicDocs.length} documents`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Update Document Ordering - Rational Pedagogical Sequence
|
||||
*
|
||||
* Sets rational order values for all 34 public documents based on:
|
||||
* - Learning progression (beginner → advanced)
|
||||
* - Logical dependencies
|
||||
* - User journey optimization
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
|
||||
// Rational ordering scheme for all 34 documents
|
||||
const ORDERING_SCHEME = {
|
||||
'getting-started': [
|
||||
{ slug: 'introduction', order: 1, reason: 'Start here - overview of framework' },
|
||||
{ slug: 'core-concepts', order: 2, reason: 'Core concepts after introduction' },
|
||||
{ slug: 'architectural-safeguards-against-llm-hierarchical-dominance-prose', order: 3, reason: 'Deep dive into architecture' },
|
||||
{ slug: 'tractatus-ai-safety-framework-core-values-and-principles', order: 4, reason: 'Values & philosophy last' }
|
||||
],
|
||||
|
||||
'technical-reference': [
|
||||
{ slug: 'technical-architecture', order: 1, reason: 'Technical overview first' },
|
||||
{ slug: 'implementation-guide', order: 2, reason: 'How to implement' },
|
||||
{ slug: 'implementation-guide-v1.1', order: 3, reason: 'Updated implementation guide' },
|
||||
{ slug: 'implementation-roadmap-24-month-deployment-plan', order: 4, reason: 'Long-term deployment plan' },
|
||||
{ slug: 'GLOSSARY', order: 5, reason: 'Reference for terminology' },
|
||||
{ slug: 'comparison-matrix', order: 6, reason: 'Context vs other frameworks' },
|
||||
{ slug: 'api-reference-complete', order: 7, reason: 'API overview' },
|
||||
{ slug: 'api-javascript-examples', order: 8, reason: 'JavaScript code examples' },
|
||||
{ slug: 'api-python-examples', order: 9, reason: 'Python code examples' },
|
||||
{ slug: 'openapi-specification', order: 10, reason: 'OpenAPI spec for integration' }
|
||||
],
|
||||
|
||||
'research-theory': [
|
||||
{ slug: 'executive-summary-tractatus-inflection-point', order: 1, reason: 'Executive summary first (quick read)' },
|
||||
{ slug: 'architectural-overview-and-research-status', order: 2, reason: 'Main research paper' },
|
||||
{ slug: 'organizational-theory-foundations', order: 3, reason: 'Academic grounding' },
|
||||
{ slug: 'pluralistic-values-research-foundations', order: 4, reason: 'Advanced research topic' }
|
||||
],
|
||||
|
||||
'advanced-topics': [
|
||||
{ slug: 'value-pluralism-faq', order: 1, reason: 'FAQ introduction to value pluralism' },
|
||||
{ slug: 'pluralistic-values-deliberation-plan-v2', order: 2, reason: 'Implementation plan' }
|
||||
],
|
||||
|
||||
'case-studies': [
|
||||
{ slug: 'the-27027-incident-a-case-study-in-pattern-recognition-bias', order: 1, reason: 'Famous incident - start here' },
|
||||
{ slug: 'when-frameworks-fail-and-why-thats-ok', order: 2, reason: 'Philosophy of failure' },
|
||||
{ slug: 'our-framework-in-action-detecting-and-correcting-ai-fabrications', order: 3, reason: 'Success story' },
|
||||
{ slug: 'real-world-ai-governance-a-case-study-in-framework-failure-and-recovery', order: 4, reason: 'Comprehensive analysis' },
|
||||
{ slug: 'case-studies-real-world-llm-failure-modes', order: 5, reason: 'Collection of failure modes' }
|
||||
],
|
||||
|
||||
'business-leadership': [
|
||||
{ slug: 'business-case-tractatus-framework', order: 1, reason: 'Business case template' }
|
||||
],
|
||||
|
||||
'archives': [
|
||||
{ slug: 'llm-integration-feasibility-research-scope', order: 1, reason: 'Research scope document' },
|
||||
{ slug: 'case-studies-real-world-llm-failure-modes-appendix', order: 2, reason: 'Appendix B - historical tracking' },
|
||||
{ slug: 'implementation-guide-python-examples', order: 3, reason: 'Historical Python examples' },
|
||||
{ slug: 'tractatus-framework-enforcement-claude-code', order: 4, reason: 'Development tool docs' },
|
||||
{ slug: 'research-topic-concurrent-session-architecture', order: 5, reason: 'Research analysis 1' },
|
||||
{ slug: 'research-topic-rule-proliferation-transactional-overhead', order: 6, reason: 'Research analysis 2' },
|
||||
{ slug: 'phase-5-poc-session-1-summary', order: 7, reason: 'Phase 5 Session 1 tracking' },
|
||||
{ slug: 'phase-5-poc-session-2-summary', order: 8, reason: 'Phase 5 Session 2 tracking' }
|
||||
]
|
||||
};
|
||||
|
||||
async function updateDocumentOrder(slug, order, category, reason) {
|
||||
try {
|
||||
const doc = await Document.findBySlug(slug);
|
||||
|
||||
if (!doc) {
|
||||
console.log(` ❌ Not found: ${slug}`);
|
||||
return { success: false, reason: 'not_found' };
|
||||
}
|
||||
|
||||
// Update order and category
|
||||
const updated = await Document.update(doc._id.toString(), {
|
||||
order,
|
||||
category
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
console.log(` ❌ Failed to update: ${slug}`);
|
||||
return { success: false, reason: 'update_failed' };
|
||||
}
|
||||
|
||||
console.log(` ✅ ${category}[${order}] ${doc.title}`);
|
||||
console.log(` Reason: ${reason}`);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🚀 Updating Document Ordering - Rational Pedagogical Sequence\n');
|
||||
console.log('═══════════════════════════════════════════════════\n');
|
||||
|
||||
await connect();
|
||||
|
||||
let updated = 0;
|
||||
let failed = 0;
|
||||
let total = 0;
|
||||
|
||||
for (const [category, docs] of Object.entries(ORDERING_SCHEME)) {
|
||||
console.log(`\n📂 Category: ${category} (${docs.length} docs)`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
for (const { slug, order, reason } of docs) {
|
||||
total++;
|
||||
const result = await updateDocumentOrder(slug, order, category, reason);
|
||||
|
||||
if (result.success) {
|
||||
updated++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════');
|
||||
console.log('\n📊 Summary:');
|
||||
console.log(` ✅ Updated: ${updated}`);
|
||||
console.log(` ❌ Failed: ${failed}`);
|
||||
console.log(` 📦 Total: ${total}`);
|
||||
|
||||
console.log('\n🎯 Ordering Rationale:');
|
||||
console.log(' • Getting Started: Introduction → Core Concepts → Architecture → Values');
|
||||
console.log(' • Technical Reference: Overview → Guides → Roadmap → Reference → API → Examples');
|
||||
console.log(' • Research & Theory: Executive Summary → Main Paper → Foundations → Advanced');
|
||||
console.log(' • Advanced Topics: FAQ → Implementation Plan');
|
||||
console.log(' • Case Studies: Famous Case → Philosophy → Success → Analysis → Collection');
|
||||
console.log(' • Archives: Chronological + topical grouping');
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
/**
|
||||
* Update Glossary with Phase 5 Terms
|
||||
* Adds: MemoryProxy, API Memory, Hybrid Architecture, BlogCuration
|
||||
*/
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
const marked = require('marked');
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev';
|
||||
const DB_NAME = process.env.MONGODB_DB || 'tractatus_dev';
|
||||
|
||||
const NEW_TERMS = `
|
||||
|
||||
### MemoryProxy
|
||||
|
||||
**What it means:** A service that manages access to the persistence layer (MongoDB and optionally Anthropic API Memory) for all framework services.
|
||||
|
||||
**Why it matters:** Instead of each service connecting to the database independently, MemoryProxy provides a single, consistent interface. This ensures all services load the same governance rules and log decisions uniformly.
|
||||
|
||||
**Real-world analogy:** Think of it like a library's card catalog system. Instead of everyone wandering the stacks looking for books individually, they all use the same catalog system to find what they need efficiently and consistently.
|
||||
|
||||
**In Tractatus:** MemoryProxy loads the 18 governance rules from MongoDB when services initialize, provides methods to query rules by ID or category, and manages audit log writing. All 6 services (InstructionPersistenceClassifier, CrossReferenceValidator, BoundaryEnforcer, MetacognitiveVerifier, ContextPressureMonitor, BlogCuration) use MemoryProxy to access persistent storage.
|
||||
|
||||
**Technical detail:** Singleton pattern ensures all services share the same MongoDB connection pool and cached rules, improving performance and consistency.
|
||||
|
||||
---
|
||||
|
||||
### API Memory
|
||||
|
||||
**What it means:** Anthropic's API Memory system that enhances Claude conversations with automatic session context preservation across multiple interactions.
|
||||
|
||||
**Why it matters:** In Phase 5, we integrated API Memory as an *optional enhancement* to our MongoDB-based persistence. API Memory helps maintain conversation continuity, but MongoDB remains the required foundation for governance rules and audit trails.
|
||||
|
||||
**Real-world analogy:** Think of MongoDB as your permanent filing cabinet (required for records) and API Memory as sticky notes on your desk (helpful for current work but not the source of truth).
|
||||
|
||||
**In Tractatus:** API Memory provides session continuity for Claude Code conversations but does NOT replace persistent storage. Our architecture gracefully degrades—if API Memory is unavailable, all services continue functioning with MongoDB alone.
|
||||
|
||||
**Key distinction:** API Memory ≠ Persistent Storage. Governance rules MUST be in MongoDB for production systems.
|
||||
|
||||
---
|
||||
|
||||
### Hybrid Architecture
|
||||
|
||||
**What it means:** Our Phase 5 architecture that combines three memory layers: MongoDB (required), Anthropic API Memory (optional), and filesystem audit trails (debug).
|
||||
|
||||
**Why it matters:** This layered approach provides both reliability (MongoDB) and enhanced user experience (API Memory) without creating dependencies on external services. If any optional component fails, the system continues operating.
|
||||
|
||||
**Real-world analogy:** Like a car with multiple safety systems—airbags, seatbelts, crumple zones. If one system fails, the others still protect you.
|
||||
|
||||
**In Tractatus:**
|
||||
- **MongoDB** (Layer 1 - Required): Persistent storage for governance rules, audit logs, session state
|
||||
- **API Memory** (Layer 2 - Optional): Session continuity enhancement for Claude conversations
|
||||
- **Filesystem** (Layer 3 - Debug): Local audit trail in \`.memory/audit/\` directory for development
|
||||
|
||||
This architecture achieved 100% framework integration in Phase 5 with zero breaking changes to existing functionality.
|
||||
|
||||
---
|
||||
|
||||
### BlogCuration
|
||||
|
||||
**What it means:** The sixth framework service (added in Phase 5) that validates blog content and social media posts against inst_016-018 to prevent fabricated statistics, absolute guarantees, and unverified claims.
|
||||
|
||||
**Why it matters:** Marketing content can inadvertently include claims that damage credibility or constitute false advertising. BlogCuration prevents publication of content with governance violations.
|
||||
|
||||
**Real-world analogy:** Like having a legal compliance officer review every press release before publication to ensure no false or misleading claims.
|
||||
|
||||
**In Tractatus:** BlogCuration scans content for patterns like:
|
||||
- **inst_016**: Fabricated statistics without sources ("95% of users report...")
|
||||
- **inst_017**: Absolute guarantees about capabilities ("guaranteed 100% secure")
|
||||
- **inst_018**: Unverified customer claims ("thousands of satisfied customers")
|
||||
|
||||
If violations are detected, publication is blocked until content is corrected. All validation attempts are logged to the audit trail with rule IDs and violation details.
|
||||
|
||||
**Integration:** BlogCuration shares enforcement logic with BoundaryEnforcer and loads rules via MemoryProxy, ensuring consistent governance across all content.
|
||||
|
||||
---`;
|
||||
|
||||
async function main() {
|
||||
console.log('=== Updating Glossary with Phase 5 Terms ===\n');
|
||||
|
||||
let client;
|
||||
|
||||
try {
|
||||
client = await MongoClient.connect(MONGODB_URI);
|
||||
const db = client.db(DB_NAME);
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Fetch current document
|
||||
const doc = await collection.findOne({ slug: 'tractatus-agentic-governance-system-glossary-of-terms' });
|
||||
|
||||
if (!doc) {
|
||||
throw new Error('Glossary document not found');
|
||||
}
|
||||
|
||||
console.log('Current document loaded');
|
||||
console.log(`Current length: ${doc.content_markdown.length} characters\n`);
|
||||
|
||||
// Find insertion point (after existing service definitions, before "## Integration" or similar)
|
||||
let updated = doc.content_markdown;
|
||||
|
||||
// Try to find a good insertion point
|
||||
const insertionPoints = [
|
||||
'### Context Pressure',
|
||||
'### Metacognitive Verification',
|
||||
'## Integration',
|
||||
'## Framework Components'
|
||||
];
|
||||
|
||||
let insertionPoint = -1;
|
||||
let foundSection = null;
|
||||
|
||||
for (const point of insertionPoints) {
|
||||
const index = updated.indexOf(point);
|
||||
if (index > -1) {
|
||||
insertionPoint = index;
|
||||
foundSection = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertionPoint === -1) {
|
||||
// Fallback: insert before last section
|
||||
insertionPoint = updated.lastIndexOf('##');
|
||||
}
|
||||
|
||||
if (insertionPoint > -1) {
|
||||
updated = updated.slice(0, insertionPoint) + NEW_TERMS + '\n\n' + updated.slice(insertionPoint);
|
||||
console.log(`✓ Inserted Phase 5 terms before: ${foundSection || 'last section'}`);
|
||||
} else {
|
||||
// Fallback: append to end
|
||||
updated = updated + '\n\n' + NEW_TERMS;
|
||||
console.log('✓ Appended Phase 5 terms to end');
|
||||
}
|
||||
|
||||
// Update version and date in header
|
||||
updated = updated.replace('**Version:** 1.0', '**Version:** 1.1');
|
||||
updated = updated.replace('**Last Updated:** 2025-10-07', '**Last Updated:** 2025-10-11');
|
||||
|
||||
console.log(`\nNew length: ${updated.length} characters`);
|
||||
console.log(`Change: +${updated.length - doc.content_markdown.length} characters\n`);
|
||||
|
||||
// Regenerate HTML
|
||||
const content_html = marked.parse(updated);
|
||||
|
||||
// Update document
|
||||
const result = await collection.updateOne(
|
||||
{ slug: 'tractatus-agentic-governance-system-glossary-of-terms' },
|
||||
{
|
||||
$set: {
|
||||
content_markdown: updated,
|
||||
content_html: content_html,
|
||||
'metadata.date_updated': new Date(),
|
||||
'metadata.version': '1.1'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.modifiedCount > 0) {
|
||||
console.log('✓ Glossary updated in MongoDB');
|
||||
console.log('✓ Version updated to 1.1');
|
||||
console.log('✓ Date updated to 2025-10-11');
|
||||
console.log('\n=== Update Complete ===');
|
||||
|
||||
console.log('\nPhase 5 terms added:');
|
||||
console.log(' - MemoryProxy');
|
||||
console.log(' - API Memory');
|
||||
console.log(' - Hybrid Architecture');
|
||||
console.log(' - BlogCuration');
|
||||
} else {
|
||||
console.log('⚠ No changes made');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n✗ Error:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (client) await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { main };
|
||||
|
|
@ -1,582 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Upload Document to Tractatus Docs System
|
||||
*
|
||||
* One-command script to:
|
||||
* 1. Upload markdown file to database
|
||||
* 2. Generate PDF automatically
|
||||
* 3. Configure for docs.html sidebar
|
||||
* 4. Create card rendering metadata
|
||||
* 5. Set up download links
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/upload-document.js <markdown-file> [options]
|
||||
*
|
||||
* Options:
|
||||
* --category <cat> Category (getting-started, technical-reference, research-theory, etc.)
|
||||
* --audience <aud> Target audience (general, researcher, implementer, leader, etc.)
|
||||
* --title <title> Override document title
|
||||
* --author <author> Document author (default: Agentic Governance Research Team)
|
||||
* --tags <tags> Comma-separated tags
|
||||
* --no-pdf Skip PDF generation
|
||||
* --pdf-dir <dir> Custom PDF output directory (default: docs/research/)
|
||||
* --order <num> Display order (lower = higher priority, default: 999)
|
||||
* --force Overwrite existing document
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
const { markdownToHtml, extractTOC, generateSlug } = require('../src/utils/markdown.util');
|
||||
|
||||
// Parse command line arguments
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
||||
console.log(`
|
||||
Usage: node scripts/upload-document.js <markdown-file> [options]
|
||||
|
||||
Options:
|
||||
--category <cat> Category: getting-started, technical-reference, research-theory,
|
||||
advanced-topics, case-studies, business-leadership
|
||||
--audience <aud> Audience: general, researcher, implementer, leader, advocate, developer
|
||||
--title <title> Override document title (extracted from H1 if not provided)
|
||||
--author <author> Document author (default: Agentic Governance Research Team)
|
||||
--tags <tags> Comma-separated tags
|
||||
--no-pdf Skip PDF generation
|
||||
--pdf-dir <dir> Custom PDF output directory (default: docs/research/)
|
||||
--order <num> Display order (lower = higher priority, default: 999)
|
||||
--force Overwrite existing document
|
||||
--contact <email> Contact email (default: research@agenticgovernance.digital)
|
||||
|
||||
Categories:
|
||||
- getting-started 🚀 Getting Started
|
||||
- technical-reference 🔌 Technical Reference
|
||||
- research-theory 🔬 Theory & Research
|
||||
- advanced-topics 🎓 Advanced Topics
|
||||
- case-studies 📊 Case Studies
|
||||
- business-leadership 💼 Business & Leadership
|
||||
|
||||
Examples:
|
||||
# Upload research paper
|
||||
node scripts/upload-document.js docs/research/my-paper.md \\
|
||||
--category research-theory \\
|
||||
--audience researcher \\
|
||||
--tags "ai-safety,governance,research"
|
||||
|
||||
# Upload technical guide (no PDF)
|
||||
node scripts/upload-document.js docs/guides/setup.md \\
|
||||
--category getting-started \\
|
||||
--audience developer \\
|
||||
--no-pdf
|
||||
|
||||
# Upload with custom order
|
||||
node scripts/upload-document.js docs/important.md \\
|
||||
--category getting-started \\
|
||||
--order 1 \\
|
||||
--force
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Extract markdown file path
|
||||
const mdFilePath = args[0];
|
||||
if (!mdFilePath) {
|
||||
console.error('❌ Error: No markdown file specified');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse options
|
||||
const options = {
|
||||
category: null,
|
||||
audience: 'general',
|
||||
title: null,
|
||||
author: 'Agentic Governance Research Team',
|
||||
tags: [],
|
||||
generatePDF: true,
|
||||
pdfDir: 'docs/research',
|
||||
order: 999,
|
||||
force: false,
|
||||
contact: 'research@agenticgovernance.digital'
|
||||
};
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--category':
|
||||
options.category = args[++i];
|
||||
break;
|
||||
case '--audience':
|
||||
options.audience = args[++i];
|
||||
break;
|
||||
case '--title':
|
||||
options.title = args[++i];
|
||||
break;
|
||||
case '--author':
|
||||
options.author = args[++i];
|
||||
break;
|
||||
case '--tags':
|
||||
options.tags = args[++i].split(',').map(t => t.trim());
|
||||
break;
|
||||
case '--no-pdf':
|
||||
options.generatePDF = false;
|
||||
break;
|
||||
case '--pdf-dir':
|
||||
options.pdfDir = args[++i];
|
||||
break;
|
||||
case '--order':
|
||||
options.order = parseInt(args[++i]);
|
||||
break;
|
||||
case '--force':
|
||||
options.force = true;
|
||||
break;
|
||||
case '--contact':
|
||||
options.contact = args[++i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate category
|
||||
const VALID_CATEGORIES = [
|
||||
'getting-started',
|
||||
'technical-reference',
|
||||
'research-theory',
|
||||
'advanced-topics',
|
||||
'case-studies',
|
||||
'business-leadership'
|
||||
];
|
||||
|
||||
if (!options.category) {
|
||||
console.error('❌ Error: --category is required');
|
||||
console.log('Valid categories:', VALID_CATEGORIES.join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!VALID_CATEGORIES.includes(options.category)) {
|
||||
console.error(`❌ Error: Invalid category "${options.category}"`);
|
||||
console.log('Valid categories:', VALID_CATEGORIES.join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF from markdown
|
||||
*/
|
||||
async function generatePDF(mdPath, outputDir) {
|
||||
const mdFileName = path.basename(mdPath, '.md');
|
||||
const pdfFileName = `${mdFileName}.pdf`;
|
||||
const pdfPath = path.join(outputDir, pdfFileName);
|
||||
|
||||
console.log(`📄 Generating PDF: ${pdfPath}`);
|
||||
|
||||
// Create Python script for PDF generation
|
||||
const pythonScript = `
|
||||
import sys
|
||||
import markdown
|
||||
from weasyprint import HTML, CSS
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
has_pypdf2 = True
|
||||
except ImportError:
|
||||
has_pypdf2 = False
|
||||
|
||||
md_path = sys.argv[1]
|
||||
pdf_path = sys.argv[2]
|
||||
title = sys.argv[3]
|
||||
author = sys.argv[4]
|
||||
|
||||
# Read markdown
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
md_content = f.read()
|
||||
|
||||
# Convert to HTML
|
||||
html_content = markdown.markdown(
|
||||
md_content,
|
||||
extensions=[
|
||||
'markdown.extensions.tables',
|
||||
'markdown.extensions.fenced_code',
|
||||
'markdown.extensions.toc',
|
||||
'markdown.extensions.sane_lists'
|
||||
]
|
||||
)
|
||||
|
||||
# Wrap in HTML
|
||||
full_html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
{html_content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# CSS styling
|
||||
css = CSS(string="""
|
||||
@page {
|
||||
size: Letter;
|
||||
margin: 1in;
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Georgia", "Times New Roman", serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24pt;
|
||||
font-weight: bold;
|
||||
color: #1976d2;
|
||||
margin-top: 24pt;
|
||||
margin-bottom: 12pt;
|
||||
page-break-after: avoid;
|
||||
border-bottom: 2px solid #1976d2;
|
||||
padding-bottom: 4pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
font-weight: bold;
|
||||
color: #1976d2;
|
||||
margin-top: 20pt;
|
||||
margin-bottom: 10pt;
|
||||
page-break-after: avoid;
|
||||
border-bottom: 2px solid #1976d2;
|
||||
padding-bottom: 4pt;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
color: #424242;
|
||||
margin-top: 16pt;
|
||||
margin-bottom: 8pt;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10pt;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12pt 0;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 8pt;
|
||||
text-align: left;
|
||||
border: 1px solid #1976d2;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6pt;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 10pt;
|
||||
background-color: #f5f5f5;
|
||||
padding: 2pt 4pt;
|
||||
border-radius: 2pt;
|
||||
}
|
||||
""")
|
||||
|
||||
# Generate PDF
|
||||
HTML(string=full_html).write_pdf(pdf_path, stylesheets=[css])
|
||||
|
||||
# Add metadata if PyPDF2 is available
|
||||
if has_pypdf2:
|
||||
reader = PdfReader(pdf_path)
|
||||
writer = PdfWriter()
|
||||
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
writer.add_metadata({
|
||||
'/Title': title,
|
||||
'/Author': author,
|
||||
'/Creator': 'Tractatus Framework',
|
||||
'/Producer': 'WeasyPrint'
|
||||
})
|
||||
|
||||
with open(pdf_path, 'wb') as f:
|
||||
writer.write(f)
|
||||
|
||||
print(f"✓ PDF generated: {pdf_path}")
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const python = spawn('python3', [
|
||||
'-c',
|
||||
pythonScript,
|
||||
mdPath,
|
||||
pdfPath,
|
||||
options.title || 'Tractatus Document',
|
||||
options.author
|
||||
]);
|
||||
|
||||
python.stdout.on('data', (data) => {
|
||||
console.log(data.toString().trim());
|
||||
});
|
||||
|
||||
python.stderr.on('data', (data) => {
|
||||
console.error(data.toString().trim());
|
||||
});
|
||||
|
||||
python.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(pdfPath);
|
||||
} else {
|
||||
reject(new Error(`PDF generation failed with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add license and metadata to markdown file
|
||||
*/
|
||||
async function addLicenseAndMetadata(mdPath) {
|
||||
const content = await fs.readFile(mdPath, 'utf-8');
|
||||
|
||||
// Check if already has license
|
||||
if (content.includes('## License') || content.includes('Apache License')) {
|
||||
console.log('⚠️ Document already has license section');
|
||||
return;
|
||||
}
|
||||
|
||||
const license = `
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
**Research Inquiries:** ${options.contact}
|
||||
**Website:** https://agenticgovernance.digital
|
||||
**Repository:** https://github.com/AgenticGovernance/tractatus
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2025 Agentic Governance Initiative
|
||||
|
||||
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.
|
||||
|
||||
**Summary:**
|
||||
- ✅ Commercial use allowed
|
||||
- ✅ Modification allowed
|
||||
- ✅ Distribution allowed
|
||||
- ✅ Patent grant included
|
||||
- ✅ Private use allowed
|
||||
- ⚠️ Must include license and copyright notice
|
||||
- ⚠️ Must state significant changes
|
||||
- ❌ No trademark rights granted
|
||||
- ❌ No liability or warranty
|
||||
|
||||
---
|
||||
|
||||
## Document Metadata
|
||||
|
||||
<div class="document-metadata">
|
||||
|
||||
- **Version:** 1.0
|
||||
- **Created:** ${new Date().toISOString().split('T')[0]}
|
||||
- **Last Modified:** ${new Date().toISOString().split('T')[0]}
|
||||
- **Author:** ${options.author}
|
||||
- **Document ID:** ${generateSlug(options.title || path.basename(mdPath, '.md'))}
|
||||
- **Status:** Active
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
await fs.writeFile(mdPath, content + license, 'utf-8');
|
||||
console.log('✓ Added license and metadata to markdown file');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main upload function
|
||||
*/
|
||||
async function uploadDocument() {
|
||||
try {
|
||||
console.log('\n=== Tractatus Document Upload ===\n');
|
||||
|
||||
// Verify markdown file exists
|
||||
const mdPath = path.resolve(mdFilePath);
|
||||
try {
|
||||
await fs.access(mdPath);
|
||||
} catch (err) {
|
||||
console.error(`❌ Error: File not found: ${mdPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📄 Processing: ${mdPath}`);
|
||||
|
||||
// Add license and metadata
|
||||
await addLicenseAndMetadata(mdPath);
|
||||
|
||||
// Read markdown content
|
||||
const rawContent = await fs.readFile(mdPath, 'utf-8');
|
||||
|
||||
// Extract title from first H1 or use provided title
|
||||
let title = options.title;
|
||||
if (!title) {
|
||||
const h1Match = rawContent.match(/^#\s+(.+)$/m);
|
||||
title = h1Match ? h1Match[1] : path.basename(mdPath, '.md');
|
||||
}
|
||||
|
||||
console.log(`📌 Title: ${title}`);
|
||||
console.log(`📂 Category: ${options.category}`);
|
||||
console.log(`👥 Audience: ${options.audience}`);
|
||||
|
||||
// Generate PDF if requested
|
||||
let pdfPath = null;
|
||||
let pdfWebPath = null;
|
||||
|
||||
if (options.generatePDF) {
|
||||
try {
|
||||
const outputDir = path.resolve(options.pdfDir);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
pdfPath = await generatePDF(mdPath, outputDir);
|
||||
// Convert to web path
|
||||
pdfWebPath = '/' + path.relative(path.resolve('public'), pdfPath);
|
||||
console.log(`✓ PDF available at: ${pdfWebPath}`);
|
||||
} catch (err) {
|
||||
console.error(`⚠️ PDF generation failed: ${err.message}`);
|
||||
console.log(' Continuing without PDF...');
|
||||
}
|
||||
}
|
||||
|
||||
// Convert markdown to HTML
|
||||
const htmlContent = markdownToHtml(rawContent);
|
||||
|
||||
// Extract table of contents
|
||||
const tableOfContents = extractTOC(rawContent);
|
||||
|
||||
// Generate slug
|
||||
const slug = generateSlug(title);
|
||||
|
||||
console.log(`🔗 Slug: ${slug}`);
|
||||
|
||||
// Connect to database
|
||||
await connect();
|
||||
|
||||
// Check if document already exists
|
||||
const existing = await Document.findBySlug(slug);
|
||||
|
||||
if (existing && !options.force) {
|
||||
console.error(`\n❌ Error: Document already exists with slug: ${slug}`);
|
||||
console.log(' Use --force to overwrite');
|
||||
await close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create document object
|
||||
const doc = {
|
||||
title: title,
|
||||
slug: slug,
|
||||
quadrant: null,
|
||||
persistence: 'HIGH',
|
||||
audience: options.audience,
|
||||
visibility: 'public',
|
||||
category: options.category,
|
||||
order: options.order,
|
||||
content_html: htmlContent,
|
||||
content_markdown: rawContent,
|
||||
toc: tableOfContents,
|
||||
security_classification: {
|
||||
contains_credentials: false,
|
||||
contains_financial_info: false,
|
||||
contains_vulnerability_info: false,
|
||||
contains_infrastructure_details: false,
|
||||
requires_authentication: false
|
||||
},
|
||||
metadata: {
|
||||
author: options.author,
|
||||
version: '1.0',
|
||||
document_code: null,
|
||||
related_documents: [],
|
||||
tags: options.tags
|
||||
},
|
||||
translations: {},
|
||||
search_index: rawContent.toLowerCase(),
|
||||
download_formats: {}
|
||||
};
|
||||
|
||||
// Add PDF download if available
|
||||
if (pdfWebPath) {
|
||||
doc.download_formats.pdf = pdfWebPath;
|
||||
}
|
||||
|
||||
// Create or update document
|
||||
if (existing && options.force) {
|
||||
await Document.update(existing._id, doc);
|
||||
console.log(`\n✅ Document updated successfully!`);
|
||||
} else {
|
||||
await Document.create(doc);
|
||||
console.log(`\n✅ Document created successfully!`);
|
||||
}
|
||||
|
||||
console.log(`\n📊 Document Details:`);
|
||||
console.log(` Title: ${doc.title}`);
|
||||
console.log(` Slug: ${doc.slug}`);
|
||||
console.log(` Category: ${doc.category}`);
|
||||
console.log(` Audience: ${doc.audience}`);
|
||||
console.log(` Order: ${doc.order}`);
|
||||
console.log(` Tags: ${doc.metadata.tags.join(', ') || 'none'}`);
|
||||
if (pdfWebPath) {
|
||||
console.log(` PDF: ${pdfWebPath}`);
|
||||
}
|
||||
|
||||
console.log(`\n✅ Document is now available at:`);
|
||||
console.log(` https://agenticgovernance.digital/docs.html?doc=${slug}`);
|
||||
console.log(` https://agenticgovernance.digital/docs.html?category=${doc.category}`);
|
||||
|
||||
console.log(`\n💡 Next Steps:`);
|
||||
console.log(` 1. Clear browser cache (Ctrl+Shift+R or Cmd+Shift+R)`);
|
||||
console.log(` 2. Visit docs.html to see your document in the sidebar`);
|
||||
console.log(` 3. Document will appear under "${options.category}" category`);
|
||||
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Upload failed:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
uploadDocument();
|
||||
}
|
||||
|
||||
module.exports = uploadDocument;
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Document Security Validation Script
|
||||
* Validates that documents being deployed are appropriate for their visibility level
|
||||
*
|
||||
* This script is called before document import/deployment to prevent
|
||||
* accidental exposure of sensitive internal documentation.
|
||||
*/
|
||||
|
||||
const SECURITY_KEYWORDS = {
|
||||
credentials: ['password', 'api_key', 'secret', 'token', 'mongodb_uri', 'stripe_secret', 'auth'],
|
||||
financial: ['stripe', 'payment', 'pricing', 'revenue', 'cost', 'billing'],
|
||||
vulnerability: ['security audit', 'vulnerability', 'exploit', 'cve-', 'penetration test'],
|
||||
infrastructure: ['deployment', 'server', 'ssh', 'production environment', 'database credentials']
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify document security level based on content and metadata
|
||||
*/
|
||||
function classifyDocumentSecurity(docConfig, contentMarkdown) {
|
||||
const classification = {
|
||||
contains_credentials: false,
|
||||
contains_financial_info: false,
|
||||
contains_vulnerability_info: false,
|
||||
contains_infrastructure_details: false,
|
||||
recommended_visibility: 'public'
|
||||
};
|
||||
|
||||
const lowerContent = contentMarkdown.toLowerCase();
|
||||
const lowerTitle = docConfig.title.toLowerCase();
|
||||
|
||||
// Check for credentials
|
||||
if (SECURITY_KEYWORDS.credentials.some(kw => lowerContent.includes(kw) || lowerTitle.includes(kw))) {
|
||||
classification.contains_credentials = true;
|
||||
}
|
||||
|
||||
// Check for financial information
|
||||
if (SECURITY_KEYWORDS.financial.some(kw => lowerContent.includes(kw) || lowerTitle.includes(kw))) {
|
||||
classification.contains_financial_info = true;
|
||||
}
|
||||
|
||||
// Check for vulnerability information
|
||||
if (SECURITY_KEYWORDS.vulnerability.some(kw => lowerContent.includes(kw) || lowerTitle.includes(kw))) {
|
||||
classification.contains_vulnerability_info = true;
|
||||
}
|
||||
|
||||
// Check for infrastructure details
|
||||
if (SECURITY_KEYWORDS.infrastructure.some(kw => lowerContent.includes(kw) || lowerTitle.includes(kw))) {
|
||||
classification.contains_infrastructure_details = true;
|
||||
}
|
||||
|
||||
// Determine recommended visibility
|
||||
if (classification.contains_credentials || classification.contains_vulnerability_info) {
|
||||
classification.recommended_visibility = 'confidential';
|
||||
} else if (classification.contains_financial_info || classification.contains_infrastructure_details) {
|
||||
classification.recommended_visibility = 'internal';
|
||||
}
|
||||
|
||||
return classification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate document configuration before deployment
|
||||
*/
|
||||
function validateDocumentSecurity(docConfig, contentMarkdown) {
|
||||
const classification = classifyDocumentSecurity(docConfig, contentMarkdown);
|
||||
const visibility = docConfig.visibility || 'public';
|
||||
|
||||
const issues = [];
|
||||
const warnings = [];
|
||||
|
||||
// CRITICAL: Credentials or vulnerabilities marked as public
|
||||
if (visibility === 'public') {
|
||||
if (classification.contains_credentials) {
|
||||
issues.push('❌ BLOCKED: Document contains credentials but is marked public');
|
||||
}
|
||||
if (classification.contains_vulnerability_info) {
|
||||
issues.push('❌ BLOCKED: Document contains vulnerability information but is marked public');
|
||||
}
|
||||
if (classification.contains_financial_info) {
|
||||
warnings.push('⚠️ WARNING: Document contains financial information but is marked public');
|
||||
}
|
||||
if (classification.contains_infrastructure_details) {
|
||||
warnings.push('⚠️ WARNING: Document contains infrastructure details but is marked public');
|
||||
}
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
if (classification.recommended_visibility !== visibility) {
|
||||
warnings.push(`⚠️ RECOMMEND: Change visibility from '${visibility}' to '${classification.recommended_visibility}'`);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: issues.length === 0,
|
||||
issues,
|
||||
warnings,
|
||||
classification,
|
||||
recommended_visibility: classification.recommended_visibility
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
classifyDocumentSecurity,
|
||||
validateDocumentSecurity,
|
||||
SECURITY_KEYWORDS
|
||||
};
|
||||
|
||||
// CLI usage
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 2) {
|
||||
console.log('Usage: node validate-document-security.js <title> <markdown-file>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const title = args[0];
|
||||
const filePath = args[1];
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`File not found: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const result = validateDocumentSecurity({ title, visibility: 'public' }, content);
|
||||
|
||||
console.log(`\n🔒 Security Validation: ${title}`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
if (result.issues.length > 0) {
|
||||
console.log('\nISSUES:');
|
||||
result.issues.forEach(issue => console.log(` ${issue}`));
|
||||
}
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log('\nWARNINGS:');
|
||||
result.warnings.forEach(warning => console.log(` ${warning}`));
|
||||
}
|
||||
|
||||
console.log('\nCLASSIFICATION:');
|
||||
console.log(` Credentials: ${result.classification.contains_credentials}`);
|
||||
console.log(` Financial Info: ${result.classification.contains_financial_info}`);
|
||||
console.log(` Vulnerabilities: ${result.classification.contains_vulnerability_info}`);
|
||||
console.log(` Infrastructure: ${result.classification.contains_infrastructure_details}`);
|
||||
console.log(` Recommended: ${result.classification.recommended_visibility}`);
|
||||
|
||||
console.log('\n' + '─'.repeat(60));
|
||||
console.log(result.valid ? '✅ VALIDATION PASSED' : '❌ VALIDATION FAILED');
|
||||
console.log();
|
||||
|
||||
process.exit(result.valid ? 0 : 1);
|
||||
}
|
||||
|
|
@ -1,437 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Tractatus Framework - Public Sync Security Validator
|
||||
*
|
||||
* Scans files before syncing to public repository to prevent:
|
||||
* - Internal file paths
|
||||
* - Database names and connection strings
|
||||
* - Port numbers and infrastructure details
|
||||
* - Email addresses and credentials
|
||||
* - Cross-project references
|
||||
* - Internal URLs and IP addresses
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 = PASS (safe to sync)
|
||||
* 1 = FAIL (security issues found, block sync)
|
||||
* 2 = ERROR (validation system failure)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bright: '\x1b[1m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m'
|
||||
};
|
||||
|
||||
// Security patterns to detect
|
||||
const SECURITY_PATTERNS = [
|
||||
// Internal file paths
|
||||
{
|
||||
pattern: /\/home\/[a-zA-Z0-9_-]+\/projects\//gi,
|
||||
severity: 'HIGH',
|
||||
description: 'Internal file path detected',
|
||||
category: 'File Paths'
|
||||
},
|
||||
{
|
||||
pattern: /\/home\/[a-zA-Z0-9_-]+\//gi,
|
||||
severity: 'HIGH',
|
||||
description: 'Home directory path detected',
|
||||
category: 'File Paths'
|
||||
},
|
||||
|
||||
// Database names and connection strings
|
||||
{
|
||||
pattern: /tractatus_dev|tractatus_prod|tractatus_test/gi,
|
||||
severity: 'HIGH',
|
||||
description: 'Database name detected',
|
||||
category: 'Database'
|
||||
},
|
||||
{
|
||||
pattern: /mongodb:\/\/[^\s]+/gi,
|
||||
severity: 'CRITICAL',
|
||||
description: 'MongoDB connection string detected',
|
||||
category: 'Database'
|
||||
},
|
||||
{
|
||||
pattern: /port:\s*27017/gi,
|
||||
severity: 'MEDIUM',
|
||||
description: 'MongoDB port number detected',
|
||||
category: 'Infrastructure'
|
||||
},
|
||||
|
||||
// Port numbers and infrastructure
|
||||
{
|
||||
pattern: /port\s*(?:=|:)\s*(?:9000|3000|8080|5000)/gi,
|
||||
severity: 'MEDIUM',
|
||||
description: 'Application port number detected',
|
||||
category: 'Infrastructure'
|
||||
},
|
||||
{
|
||||
pattern: /localhost:\d+/gi,
|
||||
severity: 'MEDIUM',
|
||||
description: 'Localhost URL with port detected',
|
||||
category: 'Infrastructure'
|
||||
},
|
||||
|
||||
// IP addresses and servers
|
||||
{
|
||||
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
||||
severity: 'HIGH',
|
||||
description: 'IP address detected',
|
||||
category: 'Infrastructure',
|
||||
exceptions: ['0.0.0.0', '127.0.0.1', '255.255.255.255'] // Common non-sensitive IPs
|
||||
},
|
||||
{
|
||||
pattern: /vps-[a-zA-Z0-9-]+\.vps\.ovh\.net/gi,
|
||||
severity: 'CRITICAL',
|
||||
description: 'OVH VPS hostname detected',
|
||||
category: 'Infrastructure'
|
||||
},
|
||||
|
||||
// Email addresses (except public ones)
|
||||
{
|
||||
pattern: /[a-zA-Z0-9._%+-]+@(?!example\.com|domain\.com|anthropic\.com|agenticgovernance\.org|pm\.me)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
|
||||
severity: 'MEDIUM',
|
||||
description: 'Personal email address detected',
|
||||
category: 'Personal Info'
|
||||
},
|
||||
|
||||
// Systemd and process management
|
||||
{
|
||||
pattern: /tractatus(?:-dev|-prod)?\.service/gi,
|
||||
severity: 'MEDIUM',
|
||||
description: 'Systemd service name detected',
|
||||
category: 'Infrastructure'
|
||||
},
|
||||
{
|
||||
pattern: /pm2\s+(?:start|restart|stop|list)/gi,
|
||||
severity: 'LOW',
|
||||
description: 'PM2 process management command detected',
|
||||
category: 'Infrastructure'
|
||||
},
|
||||
|
||||
// SSH and credentials
|
||||
{
|
||||
pattern: /ssh-rsa\s+[A-Za-z0-9+\/=]+/gi,
|
||||
severity: 'CRITICAL',
|
||||
description: 'SSH public key detected',
|
||||
category: 'Credentials'
|
||||
},
|
||||
{
|
||||
pattern: /-----BEGIN\s+(?:RSA\s+)?(?:PRIVATE|PUBLIC)\s+KEY-----/gi,
|
||||
severity: 'CRITICAL',
|
||||
description: 'SSH key block detected',
|
||||
category: 'Credentials'
|
||||
},
|
||||
{
|
||||
pattern: /(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']+/gi,
|
||||
severity: 'CRITICAL',
|
||||
description: 'Password detected',
|
||||
category: 'Credentials'
|
||||
},
|
||||
{
|
||||
pattern: /(?:api[-_]?key|apikey|access[-_]?token)\s*[:=]\s*["']?[^\s"']+/gi,
|
||||
severity: 'CRITICAL',
|
||||
description: 'API key or token detected',
|
||||
category: 'Credentials'
|
||||
},
|
||||
|
||||
// Cross-project references
|
||||
{
|
||||
pattern: /\/projects\/(?:sydigital|family-history)\//gi,
|
||||
severity: 'HIGH',
|
||||
description: 'Cross-project reference detected',
|
||||
category: 'Project References'
|
||||
},
|
||||
|
||||
// Internal documentation markers
|
||||
{
|
||||
pattern: /CLAUDE\.md|CLAUDE_.*_Guide\.md/gi,
|
||||
severity: 'HIGH',
|
||||
description: 'Internal documentation reference detected',
|
||||
category: 'Internal Docs'
|
||||
},
|
||||
{
|
||||
pattern: /SESSION-HANDOFF-.*\.md/gi,
|
||||
severity: 'HIGH',
|
||||
description: 'Session handoff document reference detected',
|
||||
category: 'Internal Docs'
|
||||
}
|
||||
];
|
||||
|
||||
// Allowed patterns that should not trigger warnings
|
||||
const ALLOWED_PATTERNS = [
|
||||
/\[DATABASE_NAME\]/gi, // Placeholder used in sanitized examples
|
||||
/\[PORT\]/gi, // Placeholder for ports
|
||||
/\[PATH\]/gi, // Placeholder for paths
|
||||
/example\.com/gi, // Example domain
|
||||
/localhost/gi // Generic localhost reference without port
|
||||
];
|
||||
|
||||
class PublicSyncValidator {
|
||||
constructor() {
|
||||
this.issues = [];
|
||||
this.filesScanned = 0;
|
||||
this.mode = process.env.SYNC_MODE || 'manual';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation entry point
|
||||
*/
|
||||
async validate() {
|
||||
console.log(`${colors.cyan}════════════════════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.bright} Tractatus Public Sync - Security Validation${colors.reset}`);
|
||||
console.log(`${colors.cyan}════════════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
// Determine files to scan
|
||||
const filesToScan = await this.getFilesToSync();
|
||||
|
||||
if (filesToScan.length === 0) {
|
||||
console.log(`${colors.yellow}⚠ No files to validate${colors.reset}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`${colors.cyan}📁 Files to validate: ${filesToScan.length}${colors.reset}\n`);
|
||||
|
||||
// Scan each file
|
||||
for (const file of filesToScan) {
|
||||
await this.scanFile(file);
|
||||
}
|
||||
|
||||
// Report results
|
||||
return this.reportResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of files that will be synced
|
||||
*/
|
||||
async getFilesToSync() {
|
||||
const files = [];
|
||||
const baseDir = process.cwd();
|
||||
|
||||
// Case studies
|
||||
const caseStudiesDir = path.join(baseDir, 'docs/case-studies');
|
||||
if (fs.existsSync(caseStudiesDir)) {
|
||||
const caseStudies = fs.readdirSync(caseStudiesDir)
|
||||
.filter(f => f.endsWith('.md'))
|
||||
.map(f => path.join(caseStudiesDir, f));
|
||||
files.push(...caseStudies);
|
||||
}
|
||||
|
||||
// Research topics
|
||||
const researchDir = path.join(baseDir, 'docs/research');
|
||||
if (fs.existsSync(researchDir)) {
|
||||
const research = fs.readdirSync(researchDir)
|
||||
.filter(f => f.endsWith('.md'))
|
||||
.map(f => path.join(researchDir, f));
|
||||
files.push(...research);
|
||||
}
|
||||
|
||||
// README (if marked as sanitized)
|
||||
const readme = path.join(baseDir, 'README.md');
|
||||
if (fs.existsSync(readme)) {
|
||||
const content = fs.readFileSync(readme, 'utf8');
|
||||
if (content.includes('<!-- PUBLIC_REPO_SAFE -->')) {
|
||||
files.push(readme);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip markdown code blocks from content
|
||||
*/
|
||||
stripCodeBlocks(content) {
|
||||
// Remove fenced code blocks (```...```)
|
||||
let stripped = content.replace(/```[\s\S]*?```/g, '');
|
||||
// Remove inline code (`...`)
|
||||
stripped = stripped.replace(/`[^`]+`/g, '');
|
||||
return stripped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a match is inside a code block
|
||||
*/
|
||||
isInCodeBlock(content, match) {
|
||||
const matchIndex = content.indexOf(match);
|
||||
if (matchIndex === -1) return false;
|
||||
|
||||
// Check if inside fenced code block
|
||||
const beforeMatch = content.substring(0, matchIndex);
|
||||
const fenceCount = (beforeMatch.match(/```/g) || []).length;
|
||||
if (fenceCount % 2 === 1) return true; // Odd number of fences = inside block
|
||||
|
||||
// Check if inside inline code
|
||||
const lineStart = beforeMatch.lastIndexOf('\n') + 1;
|
||||
const lineEnd = content.indexOf('\n', matchIndex);
|
||||
const line = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
|
||||
const matchPos = matchIndex - lineStart;
|
||||
|
||||
let inInlineCode = false;
|
||||
let backtickCount = 0;
|
||||
for (let i = 0; i < matchPos; i++) {
|
||||
if (line[i] === '`') backtickCount++;
|
||||
}
|
||||
return backtickCount % 2 === 1; // Odd number of backticks = inside inline code
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single file for security issues
|
||||
*/
|
||||
async scanFile(filePath) {
|
||||
this.filesScanned++;
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
console.log(`${colors.cyan}▶ Scanning:${colors.reset} ${relativePath}`);
|
||||
|
||||
// Check against each security pattern
|
||||
for (const { pattern, severity, description, category, exceptions } of SECURITY_PATTERNS) {
|
||||
const matches = content.match(pattern);
|
||||
|
||||
if (matches) {
|
||||
// Filter out allowed patterns and exceptions
|
||||
const validMatches = matches.filter(match => {
|
||||
// Check if it's an exception
|
||||
if (exceptions && exceptions.some(exc => match.toLowerCase().includes(exc.toLowerCase()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's an allowed pattern
|
||||
if (ALLOWED_PATTERNS.some(allowed => allowed.test(match))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For infrastructure patterns (ports, etc), skip if in code blocks
|
||||
if (category === 'Infrastructure' && this.isInCodeBlock(content, match)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (validMatches.length > 0) {
|
||||
this.issues.push({
|
||||
file: relativePath,
|
||||
severity,
|
||||
category,
|
||||
description,
|
||||
matches: validMatches,
|
||||
lineNumbers: this.getLineNumbers(content, validMatches)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line numbers for matches
|
||||
*/
|
||||
getLineNumbers(content, matches) {
|
||||
const lines = content.split('\n');
|
||||
const lineNumbers = [];
|
||||
|
||||
for (const match of matches) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].includes(match)) {
|
||||
lineNumbers.push(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lineNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report validation results
|
||||
*/
|
||||
reportResults() {
|
||||
console.log(`\n${colors.cyan}════════════════════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.bright} Validation Results${colors.reset}`);
|
||||
console.log(`${colors.cyan}════════════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
console.log(`📊 Files scanned: ${this.filesScanned}`);
|
||||
console.log(`🔍 Issues found: ${this.issues.length}\n`);
|
||||
|
||||
if (this.issues.length === 0) {
|
||||
console.log(`${colors.green}${colors.bright}✓ PASS${colors.reset} ${colors.green}All files passed security validation${colors.reset}\n`);
|
||||
console.log(`${colors.cyan}════════════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Group issues by severity
|
||||
const critical = this.issues.filter(i => i.severity === 'CRITICAL');
|
||||
const high = this.issues.filter(i => i.severity === 'HIGH');
|
||||
const medium = this.issues.filter(i => i.severity === 'MEDIUM');
|
||||
const low = this.issues.filter(i => i.severity === 'LOW');
|
||||
|
||||
// Report issues
|
||||
if (critical.length > 0) {
|
||||
console.log(`${colors.red}${colors.bright}🚨 CRITICAL Issues (${critical.length}):${colors.reset}`);
|
||||
this.printIssues(critical);
|
||||
}
|
||||
|
||||
if (high.length > 0) {
|
||||
console.log(`${colors.red}${colors.bright}⚠ HIGH Severity Issues (${high.length}):${colors.reset}`);
|
||||
this.printIssues(high);
|
||||
}
|
||||
|
||||
if (medium.length > 0) {
|
||||
console.log(`${colors.yellow}${colors.bright}⚠ MEDIUM Severity Issues (${medium.length}):${colors.reset}`);
|
||||
this.printIssues(medium);
|
||||
}
|
||||
|
||||
if (low.length > 0) {
|
||||
console.log(`${colors.yellow}ℹ LOW Severity Issues (${low.length}):${colors.reset}`);
|
||||
this.printIssues(low);
|
||||
}
|
||||
|
||||
console.log(`\n${colors.red}${colors.bright}✗ FAIL${colors.reset} ${colors.red}Security validation failed - sync blocked${colors.reset}\n`);
|
||||
console.log(`${colors.cyan}════════════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print issues in a formatted way
|
||||
*/
|
||||
printIssues(issues) {
|
||||
for (const issue of issues) {
|
||||
console.log(`\n ${colors.bright}${issue.file}${colors.reset}`);
|
||||
console.log(` Category: ${issue.category}`);
|
||||
console.log(` Issue: ${issue.description}`);
|
||||
console.log(` Lines: ${issue.lineNumbers.join(', ')}`);
|
||||
console.log(` Matches: ${issue.matches.slice(0, 3).join(', ')}${issue.matches.length > 3 ? '...' : ''}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
try {
|
||||
const validator = new PublicSyncValidator();
|
||||
const exitCode = await validator.validate();
|
||||
process.exit(exitCode);
|
||||
} catch (error) {
|
||||
console.error(`${colors.red}${colors.bright}ERROR:${colors.reset} ${error.message}`);
|
||||
console.error(error.stack);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = PublicSyncValidator;
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify All 34 Public Documents
|
||||
*
|
||||
* Lists all 34 documents that should be visible on the frontend
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
async function verify34Documents() {
|
||||
const client = new MongoClient('mongodb://localhost:27017');
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db('tractatus_dev');
|
||||
const collection = db.collection('documents');
|
||||
|
||||
// Get documents with proper categories (not 'none')
|
||||
const documents = await collection.find({
|
||||
visibility: { $in: ['public', 'archived'] },
|
||||
category: { $in: ['getting-started', 'technical-reference', 'research-theory', 'advanced-topics', 'case-studies', 'business-leadership', 'archives'] }
|
||||
})
|
||||
.sort({ category: 1, order: 1 })
|
||||
.toArray();
|
||||
|
||||
console.log(`\n=== VERIFICATION: 34 Public Documents ===`);
|
||||
console.log(`Found: ${documents.length} documents\n`);
|
||||
|
||||
const byCategory = {};
|
||||
documents.forEach(doc => {
|
||||
const cat = doc.category;
|
||||
if (!byCategory[cat]) {
|
||||
byCategory[cat] = [];
|
||||
}
|
||||
byCategory[cat].push({
|
||||
order: doc.order,
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
visibility: doc.visibility || 'public',
|
||||
sections: doc.sections ? doc.sections.length : 0
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(byCategory).sort().forEach(category => {
|
||||
console.log(`\n━━━ ${category.toUpperCase()} (${byCategory[category].length} docs) ━━━`);
|
||||
byCategory[category].forEach(doc => {
|
||||
const sectionStatus = doc.sections > 0 ? `✅ ${doc.sections} sections` : `❌ No sections`;
|
||||
console.log(`${doc.order}. ${doc.title}`);
|
||||
console.log(` ${doc.slug} | ${doc.visibility} | ${sectionStatus}`);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\n═══════════════════════════════════════════════════`);
|
||||
console.log(`Total: ${documents.length} documents`);
|
||||
|
||||
await client.close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
verify34Documents();
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
|
||||
const { connect, close } = require('../src/utils/db.util');
|
||||
const Document = require('../src/models/Document.model');
|
||||
|
||||
async function main() {
|
||||
await connect();
|
||||
|
||||
// Query all documents with valid categories (including archived)
|
||||
const { MongoClient } = require('mongodb');
|
||||
const client = new MongoClient(process.env.MONGO_URI || 'mongodb://localhost:27017');
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const db = client.db(process.env.MONGO_DB || 'tractatus_dev');
|
||||
const collection = db.collection('documents');
|
||||
|
||||
const docs = await collection.find({
|
||||
category: {
|
||||
$in: ['getting-started', 'technical-reference', 'research-theory',
|
||||
'advanced-topics', 'case-studies', 'business-leadership', 'archives']
|
||||
}
|
||||
}).sort({ category: 1, order: 1 }).toArray();
|
||||
|
||||
console.log(`\n${'='.repeat(70)}`);
|
||||
console.log(`COMPLETE VERIFICATION - All 34 Public Documents`);
|
||||
console.log(`${'='.repeat(70)}\n`);
|
||||
console.log(`Total documents: ${docs.length}\n`);
|
||||
|
||||
const byCategory = {};
|
||||
docs.forEach(doc => {
|
||||
const cat = doc.category;
|
||||
if (!byCategory[cat]) byCategory[cat] = [];
|
||||
byCategory[cat].push(doc);
|
||||
});
|
||||
|
||||
const categoryOrder = ['getting-started', 'technical-reference', 'research-theory',
|
||||
'advanced-topics', 'case-studies', 'business-leadership', 'archives'];
|
||||
|
||||
categoryOrder.forEach(cat => {
|
||||
if (byCategory[cat]) {
|
||||
const docsInCat = byCategory[cat].sort((a, b) => (a.order || 999) - (b.order || 999));
|
||||
console.log(`\n${'━'.repeat(70)}`);
|
||||
console.log(`${cat.toUpperCase().replace(/-/g, ' ')} (${docsInCat.length} docs)`);
|
||||
console.log(`${'━'.repeat(70)}`);
|
||||
|
||||
docsInCat.forEach(doc => {
|
||||
const sections = doc.sections ? doc.sections.length : 0;
|
||||
const sectionStatus = sections > 0 ? `✅ ${sections} sections` : `❌ No sections`;
|
||||
const visibility = doc.visibility || 'public';
|
||||
const visLabel = visibility === 'archived' ? ' [ARCHIVED]' : '';
|
||||
console.log(`${doc.order}. ${doc.title}${visLabel}`);
|
||||
console.log(` ${doc.slug} | ${sectionStatus}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n${'='.repeat(70)}`);
|
||||
console.log(`Total: ${docs.length} documents`);
|
||||
|
||||
if (docs.length === 34) {
|
||||
console.log('✅ SUCCESS - All 34 expected documents present');
|
||||
} else {
|
||||
console.log(`⚠️ MISMATCH - Expected 34, found ${docs.length}`);
|
||||
}
|
||||
|
||||
console.log(`${'='.repeat(70)}\n`);
|
||||
|
||||
await client.close();
|
||||
await close();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Reference in a new issue