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
210 lines
5.1 KiB
JavaScript
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;
|