tractatus/src/models/AuditLog.model.js
TheFlow e70577cdd0 fix: MongoDB persistence and inst_016-018 content validation enforcement
This commit implements critical fixes to stabilize the MongoDB persistence layer
and adds inst_016-018 content validation to BoundaryEnforcer as specified in
instruction history.

## Context
- First session using Anthropic's new API Memory system
- Fixed 3 MongoDB persistence test failures
- Implemented BoundaryEnforcer inst_016-018 trigger logic per user request
- All unit tests now passing (61/61 BoundaryEnforcer, 25/25 BlogCuration)

## Fixes

### 1. CrossReferenceValidator: Port Regex Enhancement
- **File**: src/services/CrossReferenceValidator.service.js:203
- **Issue**: Regex couldn't extract port from "port 27017" (space-delimited format)
- **Fix**: Changed `/port[:=]\s*(\d{4,5})/i` to `/port[:\s=]\s*(\d{4,5})/i`
- **Result**: Now matches "port: X", "port = X", and "port X" formats
- **Tests**: 28/28 CrossReferenceValidator tests passing

### 2. BlogCuration: MongoDB Method Correction
- **File**: src/services/BlogCuration.service.js:187
- **Issue**: Called non-existent `Document.findAll()` method
- **Fix**: Changed to `Document.list({ limit: 20, skip: 0 })`
- **Result**: BlogCuration can now fetch existing documents for topic generation
- **Tests**: 25/25 BlogCuration tests passing

### 3. MemoryProxy: Optional Anthropic API Integration
- **File**: src/services/MemoryProxy.service.js
- **Issue**: Treated Anthropic Memory Tool API as mandatory, causing errors without API key
- **Fix**: Made Anthropic client optional with graceful degradation
- **Architecture**: MongoDB (required) + Anthropic API (optional enhancement)
- **Result**: System functions fully without CLAUDE_API_KEY environment variable

### 4. AuditLog Model: Duplicate Index Fix
- **File**: src/models/AuditLog.model.js:132
- **Issue**: Mongoose warning about duplicate timestamp index
- **Fix**: Removed inline `index: true`, kept TTL index definition at line 149
- **Result**: No more Mongoose duplicate index warnings

### 5. BlogCuration Tests: Mock API Correction
- **File**: tests/unit/BlogCuration.service.test.js
- **Issue**: Tests mocked non-existent `generateBlogTopics()` function
- **Fix**: Updated mocks to use actual `sendMessage()` and `extractJSON()` methods
- **Result**: All 25 BlogCuration tests passing

## New Features

### 6. BoundaryEnforcer: inst_016-018 Content Validation (MAJOR)
- **File**: src/services/BoundaryEnforcer.service.js:508-580
- **Purpose**: Prevent fabricated statistics, absolute guarantees, and unverified claims
- **Implementation**: Added `_checkContentViolations()` private method
- **Enforcement Rules**:
  - **inst_017**: Blocks absolute assurance terms (guarantee, 100% secure, never fails)
  - **inst_016**: Blocks statistics/ROI/$ amounts without sources
  - **inst_018**: Blocks production claims (production-ready, battle-tested) without evidence
- **Mechanism**: All violations classified as VALUES boundary violations (honesty/transparency)
- **Tests**: 22 new comprehensive tests in tests/unit/BoundaryEnforcer.test.js
- **Result**: 61/61 BoundaryEnforcer tests passing

### Regex Pattern for inst_016 (Statistics Detection):
```regex
/\d+(\.\d+)?%|\$[\d,]+|\d+x\s*roi|payback\s*(period)?\s*of\s*\d+|\d+[\s-]*(month|year)s?\s*payback|\d+(\.\d+)?m\s*(saved|savings)/i
```

### Detection Examples:
-  BLOCKS: "This system guarantees 100% security"
-  BLOCKS: "Delivers 1315% ROI without sources"
-  BLOCKS: "Production-ready framework" (without testing_evidence)
-  ALLOWS: "Research shows 85% improvement [source: example.com]"
-  ALLOWS: "Validated framework with testing_evidence provided"

## MongoDB Models (New Files)
- src/models/AuditLog.model.js - Audit log persistence with TTL
- src/models/GovernanceRule.model.js - Governance rules storage
- src/models/SessionState.model.js - Session state tracking
- src/models/VerificationLog.model.js - Verification logs
- src/services/AnthropicMemoryClient.service.js - Optional API integration

## Test Results
- BoundaryEnforcer: 61/61 tests passing (22 new inst_016-018 tests)
- BlogCuration: 25/25 tests passing
- CrossReferenceValidator: 28/28 tests passing

## Framework Compliance
-  Implements inst_016, inst_017, inst_018 enforcement
-  Addresses 2025-10-09 framework failure (fabricated statistics on leader.html)
-  All content generation now subject to honesty/transparency validation
-  Human approval required for statistical claims without sources

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 00:17:03 +13:00

415 lines
9.4 KiB
JavaScript

/**
* AuditLog Model
*
* Stores governance enforcement decisions and boundary checks
* Replaces filesystem-based .memory/audit/decisions-YYYY-MM-DD.jsonl
*
* Benefits over JSONL files:
* - Fast time-range queries (indexed by timestamp)
* - Aggregation for analytics dashboard
* - Filter by sessionId, action, allowed status
* - Join with GovernanceRule for violation analysis
* - Automatic expiration with TTL index
*/
const mongoose = require('mongoose');
const auditLogSchema = new mongoose.Schema({
// Core identification
sessionId: {
type: String,
required: true,
index: true,
description: 'Session identifier for tracing related decisions'
},
action: {
type: String,
required: true,
index: true,
description: 'Type of action being audited (e.g., boundary_enforcement, content_generation)'
},
// Decision outcome
allowed: {
type: Boolean,
required: true,
index: true,
description: 'Whether the action was allowed or blocked'
},
// Governance context
rulesChecked: {
type: [String],
default: [],
description: 'IDs of rules that were evaluated (e.g., [inst_016, inst_017])'
},
violations: {
type: [{
ruleId: String,
rulText: String,
severity: {
type: String,
enum: ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'],
default: 'MEDIUM'
},
details: String
}],
default: [],
description: 'Rules that were violated (if any)'
},
// Metadata
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {},
description: 'Additional context (boundary, domain, tractatus_section, etc.)'
},
// Classification
domain: {
type: String,
enum: ['STRATEGIC', 'OPERATIONAL', 'TACTICAL', 'SYSTEM', 'UNKNOWN'],
default: 'UNKNOWN',
index: true,
description: 'Domain of the decision'
},
boundary: {
type: String,
default: null,
description: 'Boundary that was checked (if applicable)'
},
tractatus_section: {
type: String,
default: null,
index: true,
description: 'Tractatus framework section that governed this decision'
},
// Performance tracking
durationMs: {
type: Number,
default: null,
description: 'How long the enforcement check took (milliseconds)'
},
// Service tracking
service: {
type: String,
default: 'BoundaryEnforcer',
index: true,
description: 'Which service performed the audit (BoundaryEnforcer, BlogCuration, etc.)'
},
// User context (if applicable)
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null,
description: 'User who triggered the action (if applicable)'
},
// IP and request context
ipAddress: {
type: String,
default: null,
description: 'IP address of request (if applicable)'
},
userAgent: {
type: String,
default: null,
description: 'User agent string (if applicable)'
},
// Timestamp (auto-created by timestamps: true, but explicit for clarity)
// Note: Index is defined separately with TTL (line 149), not here
timestamp: {
type: Date,
default: Date.now,
description: 'When this decision was made'
}
}, {
timestamps: true, // Adds createdAt and updatedAt
collection: 'auditLogs'
});
// Indexes for common queries
auditLogSchema.index({ timestamp: -1 }); // Most recent first
auditLogSchema.index({ sessionId: 1, timestamp: -1 }); // Session timeline
auditLogSchema.index({ allowed: 1, timestamp: -1 }); // Violations timeline
auditLogSchema.index({ service: 1, timestamp: -1 }); // Service-specific logs
auditLogSchema.index({ 'violations.ruleId': 1 }, { sparse: true }); // Violation analysis
// TTL index - automatically delete logs older than 90 days
auditLogSchema.index({ timestamp: 1 }, { expireAfterSeconds: 90 * 24 * 60 * 60 });
// Virtual for violation count
auditLogSchema.virtual('violationCount').get(function() {
return this.violations ? this.violations.length : 0;
});
// Static methods
/**
* Find recent decisions
*/
auditLogSchema.statics.findRecent = function(limit = 100) {
return this.find()
.sort({ timestamp: -1 })
.limit(limit);
};
/**
* Find decisions by session
*/
auditLogSchema.statics.findBySession = function(sessionId, options = {}) {
const query = { sessionId };
return this.find(query)
.sort({ timestamp: options.ascending ? 1 : -1 })
.limit(options.limit || 0);
};
/**
* Find decisions by date range
*/
auditLogSchema.statics.findByDateRange = function(startDate, endDate, options = {}) {
const query = {
timestamp: {
$gte: startDate,
$lte: endDate
}
};
if (options.allowed !== undefined) {
query.allowed = options.allowed;
}
if (options.service) {
query.service = options.service;
}
return this.find(query)
.sort({ timestamp: -1 })
.limit(options.limit || 0);
};
/**
* Find all violations
*/
auditLogSchema.statics.findViolations = function(options = {}) {
const query = {
allowed: false,
'violations.0': { $exists: true } // Has at least one violation
};
if (options.ruleId) {
query['violations.ruleId'] = options.ruleId;
}
if (options.startDate && options.endDate) {
query.timestamp = {
$gte: options.startDate,
$lte: options.endDate
};
}
return this.find(query)
.sort({ timestamp: -1 })
.limit(options.limit || 0);
};
/**
* Get statistics for dashboard
*/
auditLogSchema.statics.getStatistics = async function(startDate, endDate) {
const matchStage = {};
if (startDate && endDate) {
matchStage.timestamp = { $gte: startDate, $lte: endDate };
}
const stats = await this.aggregate([
{ $match: matchStage },
{
$group: {
_id: null,
totalDecisions: { $sum: 1 },
allowed: {
$sum: { $cond: ['$allowed', 1, 0] }
},
blocked: {
$sum: { $cond: ['$allowed', 0, 1] }
},
totalViolations: {
$sum: { $size: { $ifNull: ['$violations', []] } }
},
avgDuration: {
$avg: '$durationMs'
},
uniqueSessions: {
$addToSet: '$sessionId'
},
serviceBreakdown: {
$push: '$service'
}
}
},
{
$project: {
_id: 0,
totalDecisions: 1,
allowed: 1,
blocked: 1,
totalViolations: 1,
avgDuration: { $round: ['$avgDuration', 2] },
uniqueSessionCount: { $size: '$uniqueSessions' },
allowedRate: {
$multiply: [
{ $divide: ['$allowed', '$totalDecisions'] },
100
]
},
services: '$serviceBreakdown' // Simplified - just return array for now
}
}
]);
return stats[0] || null;
};
/**
* Get violation breakdown by rule
*/
auditLogSchema.statics.getViolationBreakdown = async function(startDate, endDate) {
const matchStage = {
allowed: false,
'violations.0': { $exists: true }
};
if (startDate && endDate) {
matchStage.timestamp = { $gte: startDate, $lte: endDate };
}
const breakdown = await this.aggregate([
{ $match: matchStage },
{ $unwind: '$violations' },
{
$group: {
_id: '$violations.ruleId',
count: { $sum: 1 },
severity: { $first: '$violations.severity' },
examples: {
$push: {
sessionId: '$sessionId',
timestamp: '$timestamp',
details: '$violations.details'
}
}
}
},
{
$project: {
_id: 0,
ruleId: '$_id',
count: 1,
severity: 1,
recentExamples: { $slice: ['$examples', 5] } // Last 5 examples
}
},
{ $sort: { count: -1 } }
]);
return breakdown;
};
/**
* Get timeline data (for charts)
*/
auditLogSchema.statics.getTimeline = async function(startDate, endDate, intervalHours = 1) {
const timeline = await this.aggregate([
{
$match: {
timestamp: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: {
$dateTrunc: {
date: '$timestamp',
unit: 'hour',
binSize: intervalHours
}
},
total: { $sum: 1 },
allowed: { $sum: { $cond: ['$allowed', 1, 0] } },
blocked: { $sum: { $cond: ['$allowed', 0, 1] } },
violations: {
$sum: { $size: { $ifNull: ['$violations', []] } }
}
}
},
{ $sort: { _id: 1 } },
{
$project: {
_id: 0,
timestamp: '$_id',
total: 1,
allowed: 1,
blocked: 1,
violations: 1,
allowedRate: {
$multiply: [
{ $divide: ['$allowed', '$total'] },
100
]
}
}
}
]);
return timeline;
};
// Instance methods
/**
* Add a violation to this log entry
*/
auditLogSchema.methods.addViolation = function(violation) {
this.violations.push(violation);
this.allowed = false; // Violations mean action blocked
return this.save();
};
/**
* Check if this decision was blocked
*/
auditLogSchema.methods.isBlocked = function() {
return !this.allowed;
};
/**
* Get human-readable summary
*/
auditLogSchema.methods.getSummary = function() {
return {
timestamp: this.timestamp.toISOString(),
sessionId: this.sessionId,
action: this.action,
result: this.allowed ? 'ALLOWED' : 'BLOCKED',
violationCount: this.violationCount,
service: this.service,
domain: this.domain
};
};
const AuditLog = mongoose.model('AuditLog', auditLogSchema);
module.exports = AuditLog;