tractatus/src/models/ModerationQueue.model.js
TheFlow 78ab5754f2 feat: add MongoDB models for core collections
Models Created (7/10):
- Document.model.js: Framework docs with quadrant classification
- BlogPost.model.js: AI-curated blog with moderation
- MediaInquiry.model.js: Press/media triage workflow
- ModerationQueue.model.js: Human oversight queue with priority
- User.model.js: Admin authentication with bcrypt
- CaseSubmission.model.js: Community case studies with AI review
- Resource.model.js: Curated directory with alignment scores

Features:
- Full CRUD operations for each model
- Tractatus quadrant integration
- AI analysis fields for curation
- Human approval workflows
- Status tracking and filtering
- Security (password hashing, sanitized returns)

Deferred (Phase 2-3):
- Citation.model.js
- Translation.model.js
- KohaDonation.model.js

Status: Core models complete, ready for Express server
2025-10-06 23:54:56 +13:00

210 lines
5.1 KiB
JavaScript

/**
* ModerationQueue Model
* Human oversight queue for AI actions
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class ModerationQueue {
/**
* Add item to moderation queue
*/
static async create(data) {
const collection = await getCollection('moderation_queue');
const item = {
item_type: data.item_type, // blog_post/media_inquiry/case_study/resource
item_id: new ObjectId(data.item_id),
quadrant: data.quadrant, // STR/OPS/TAC/SYS/STO
ai_action: {
type: data.ai_action.type, // suggestion/triage/analysis
confidence: data.ai_action.confidence, // 0-1
reasoning: data.ai_action.reasoning,
claude_version: data.ai_action.claude_version || 'claude-sonnet-4-5'
},
human_required_reason: data.human_required_reason,
priority: data.priority || 'medium', // high/medium/low
assigned_to: data.assigned_to,
status: 'pending', // pending/reviewed/approved/rejected
created_at: new Date(),
reviewed_at: null,
review_decision: {
action: null, // approve/reject/modify/escalate
notes: null,
reviewer: null
}
};
const result = await collection.insertOne(item);
return { ...item, _id: result.insertedId };
}
/**
* Find item by ID
*/
static async findById(id) {
const collection = await getCollection('moderation_queue');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find pending items
*/
static async findPending(options = {}) {
const collection = await getCollection('moderation_queue');
const { limit = 20, skip = 0, priority } = options;
const filter = { status: 'pending' };
if (priority) filter.priority = priority;
return await collection
.find(filter)
.sort({
priority: -1, // high first
created_at: 1 // oldest first
})
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find by item type
*/
static async findByType(itemType, options = {}) {
const collection = await getCollection('moderation_queue');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ item_type: itemType, status: 'pending' })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Find by quadrant
*/
static async findByQuadrant(quadrant, options = {}) {
const collection = await getCollection('moderation_queue');
const { limit = 20, skip = 0 } = options;
return await collection
.find({ quadrant, status: 'pending' })
.sort({ created_at: -1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Review item (approve/reject/modify/escalate)
*/
static async review(id, decision) {
const collection = await getCollection('moderation_queue');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
status: 'reviewed',
reviewed_at: new Date(),
'review_decision.action': decision.action,
'review_decision.notes': decision.notes,
'review_decision.reviewer': decision.reviewer
}
}
);
return result.modifiedCount > 0;
}
/**
* Approve item
*/
static async approve(id, reviewerId, notes = '') {
return await this.review(id, {
action: 'approve',
notes,
reviewer: reviewerId
});
}
/**
* Reject item
*/
static async reject(id, reviewerId, notes) {
return await this.review(id, {
action: 'reject',
notes,
reviewer: reviewerId
});
}
/**
* Escalate to strategic review
*/
static async escalate(id, reviewerId, reason) {
const collection = await getCollection('moderation_queue');
const result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
quadrant: 'STRATEGIC',
priority: 'high',
human_required_reason: `ESCALATED: ${reason}`,
'review_decision.action': 'escalate',
'review_decision.notes': reason,
'review_decision.reviewer': reviewerId
}
}
);
return result.modifiedCount > 0;
}
/**
* Count pending items
*/
static async countPending(filter = {}) {
const collection = await getCollection('moderation_queue');
return await collection.countDocuments({
...filter,
status: 'pending'
});
}
/**
* Get stats by quadrant
*/
static async getStatsByQuadrant() {
const collection = await getCollection('moderation_queue');
return await collection.aggregate([
{ $match: { status: 'pending' } },
{
$group: {
_id: '$quadrant',
count: { $sum: 1 },
high_priority: {
$sum: { $cond: [{ $eq: ['$priority', 'high'] }, 1, 0] }
}
}
}
]).toArray();
}
/**
* Delete item
*/
static async delete(id) {
const collection = await getCollection('moderation_queue');
const result = await collection.deleteOne({ _id: new ObjectId(id) });
return result.deletedCount > 0;
}
}
module.exports = ModerationQueue;