diff --git a/src/controllers/blog.controller.js b/src/controllers/blog.controller.js index 82e0a6e0..66954317 100644 --- a/src/controllers/blog.controller.js +++ b/src/controllers/blog.controller.js @@ -12,6 +12,7 @@ const logger = require('../utils/logger.util'); const claudeAPI = require('../services/ClaudeAPI.service'); const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); const BlogCuration = require('../services/BlogCuration.service'); +const PluralisticDeliberationOrchestrator = require('../services/PluralisticDeliberationOrchestrator.service'); /** * List published blog posts (public) @@ -311,18 +312,91 @@ async function publishPost(req, res) { }); } + // Cultural sensitivity check (inst_081 pluralism) + // Detects Western-centric framing, Indigenous exclusion + // FLAGS for review, never blocks (human decides) + let culturalCheck = null; + let culturalReviewRequired = false; + + try { + // Get full text for cultural check (title + excerpt + content) + const fullText = [post.title, post.excerpt, post.content] + .filter(Boolean) + .join('\n') + .replace(/<[^>]*>/g, ''); // Strip HTML tags + + culturalCheck = await PluralisticDeliberationOrchestrator.assessCulturalSensitivity(fullText, { + audience: { + // Phase 2: will add target_publications, language fields to BlogPost + tags: post.tags + }, + content_type: 'blog_post', + post_id: id + }); + + if (culturalCheck.risk_level === 'HIGH') { + culturalReviewRequired = true; + logger.warn(`Blog post flagged for cultural sensitivity review`, { + post_id: id, + slug: post.slug, + risk_level: culturalCheck.risk_level, + concerns_count: culturalCheck.concerns.length + }); + } + + // Store cultural check results + await BlogPost.update(id, { + 'moderation.cultural_sensitivity': { + risk_level: culturalCheck.risk_level, + concerns: culturalCheck.concerns, + suggestions: culturalCheck.suggestions, + recommended_action: culturalCheck.recommended_action, + checked_at: culturalCheck.timestamp + } + }); + + } catch (culturalError) { + logger.error('Cultural sensitivity check failed:', culturalError); + // Non-blocking: Allow publication but log the failure + await BlogPost.update(id, { + 'moderation.cultural_sensitivity_error': culturalError.message + }); + } + // Publish the post await BlogPost.publish(id, req.userId); const updatedPost = await BlogPost.findById(id); - logger.info(`Blog post published: ${id} by ${req.user.email}`); + logger.info(`Blog post published: ${id} by ${req.user.email}`, { + cultural_review_required: culturalReviewRequired + }); - res.json({ + const response = { success: true, post: updatedPost, - message: 'Post published successfully' - }); + message: culturalReviewRequired + ? 'Post published - FLAGGED for cultural sensitivity review' + : 'Post published successfully' + }; + + if (culturalCheck) { + response.cultural_sensitivity = { + risk_level: culturalCheck.risk_level, + review_required: culturalReviewRequired, + concerns_count: culturalCheck.concerns.length, + suggestions_count: culturalCheck.suggestions.length + }; + + // Include details if review required + if (culturalReviewRequired) { + response.cultural_sensitivity.concerns = culturalCheck.concerns; + response.cultural_sensitivity.suggestions = culturalCheck.suggestions; + response.cultural_sensitivity.note = 'Human review recommended (inst_081 pluralism)'; + } + } + + res.json(response); } catch (error) { logger.error('Publish post error:', error); diff --git a/src/controllers/media.controller.js b/src/controllers/media.controller.js index ea619b08..f4cc4bef 100644 --- a/src/controllers/media.controller.js +++ b/src/controllers/media.controller.js @@ -9,6 +9,7 @@ const GovernanceLog = require('../models/GovernanceLog.model'); const BoundaryEnforcer = require('../services/BoundaryEnforcer.service'); const MediaTriageService = require('../services/MediaTriage.service'); const ContentGovernanceChecker = require('../services/ContentGovernanceChecker.service'); +const PluralisticDeliberationOrchestrator = require('../services/PluralisticDeliberationOrchestrator.service'); const logger = require('../utils/logger.util'); /** @@ -269,13 +270,48 @@ async function respondToInquiry(req, res) { }); } + // Cultural sensitivity check (inst_081 pluralism) + // Detects Western-centric framing, Indigenous exclusion for diverse audiences + // FLAGS for review, never blocks (human decides) + let culturalCheck = null; + let culturalReviewRequired = false; + + if (inquiry.contact) { + culturalCheck = await PluralisticDeliberationOrchestrator.assessCulturalSensitivity(content, { + audience: { + country: inquiry.contact.country, + outlet: inquiry.contact.outlet, + cultural_context: inquiry.contact.cultural_context + }, + content_type: 'media_response', + inquiry_id: id + }); + + if (culturalCheck.risk_level === 'HIGH') { + culturalReviewRequired = true; + logger.warn(`Media response flagged for cultural sensitivity review`, { + inquiry_id: id, + risk_level: culturalCheck.risk_level, + concerns_count: culturalCheck.concerns.length, + responder: req.user.email + }); + } + } + const success = await MediaInquiry.respond(id, { content, responder: req.user.email, governance_check: { passed: true, scanned_at: governanceCheck.scannedAt - } + }, + cultural_sensitivity: culturalCheck ? { + risk_level: culturalCheck.risk_level, + concerns: culturalCheck.concerns, + suggestions: culturalCheck.suggestions, + recommended_action: culturalCheck.recommended_action, + checked_at: culturalCheck.timestamp + } : null }); if (!success) { @@ -286,18 +322,39 @@ async function respondToInquiry(req, res) { } logger.info(`Media inquiry ${id} responded to by ${req.user.email}`, { - governance_check_passed: true + governance_check_passed: true, + cultural_review_required: culturalReviewRequired }); - res.json({ + const response = { success: true, - message: 'Response recorded successfully (framework check passed)', + message: culturalReviewRequired + ? 'Response recorded - FLAGGED for cultural sensitivity review' + : 'Response recorded successfully (framework check passed)', note: 'Remember to send actual email to media contact separately', governance: { checked: true, violations: 0 } - }); + }; + + if (culturalCheck) { + response.cultural_sensitivity = { + risk_level: culturalCheck.risk_level, + review_required: culturalReviewRequired, + concerns_count: culturalCheck.concerns.length, + suggestions_count: culturalCheck.suggestions.length + }; + + // Include details if review required + if (culturalReviewRequired) { + response.cultural_sensitivity.concerns = culturalCheck.concerns; + response.cultural_sensitivity.suggestions = culturalCheck.suggestions; + response.cultural_sensitivity.note = 'Human review recommended before sending (inst_081 pluralism)'; + } + } + + res.json(response); } catch (error) { logger.error('Respond to inquiry error:', error); diff --git a/src/models/MediaInquiry.model.js b/src/models/MediaInquiry.model.js index 4314ec45..90636c9c 100644 --- a/src/models/MediaInquiry.model.js +++ b/src/models/MediaInquiry.model.js @@ -18,7 +18,9 @@ class MediaInquiry { name: data.contact.name, email: data.contact.email, outlet: data.contact.outlet, - phone: data.contact.phone + phone: data.contact.phone, + country: data.contact.country, // ISO country code for cultural sensitivity + cultural_context: data.contact.cultural_context // e.g., 'indigenous', 'non-western' }, inquiry: { subject: data.inquiry.subject, @@ -127,16 +129,28 @@ class MediaInquiry { static async respond(id, responseData) { const collection = await getCollection('media_inquiries'); + const updateDoc = { + $set: { + status: 'responded', + 'response.sent_at': new Date(), + 'response.content': responseData.content, + 'response.responder': responseData.responder + } + }; + + // Add governance check results if provided + if (responseData.governance_check) { + updateDoc.$set['response.governance_check'] = responseData.governance_check; + } + + // Add cultural sensitivity check results if provided + if (responseData.cultural_sensitivity) { + updateDoc.$set['response.cultural_sensitivity'] = responseData.cultural_sensitivity; + } + const result = await collection.updateOne( { _id: new ObjectId(id) }, - { - $set: { - status: 'responded', - 'response.sent_at': new Date(), - 'response.content': responseData.content, - 'response.responder': responseData.responder - } - } + updateDoc ); return result.modifiedCount > 0; diff --git a/src/services/PluralisticDeliberationOrchestrator.service.js b/src/services/PluralisticDeliberationOrchestrator.service.js index e88a0104..2ac379a4 100644 --- a/src/services/PluralisticDeliberationOrchestrator.service.js +++ b/src/services/PluralisticDeliberationOrchestrator.service.js @@ -562,6 +562,273 @@ class PluralisticDeliberationOrchestrator { }); } + /** + * Assess cultural sensitivity of external communication content + * Identifies culturally inappropriate messaging for diverse audiences + * + * @param {string} content - Content to assess + * @param {Object} context - Audience/communication context + * @param {Object} context.audience - Audience information (country, region, cultural_context) + * @param {string} context.content_type - Type (media_response, blog_post, template, newsletter) + * @returns {Promise} Cultural sensitivity assessment + * + * Core Principles: + * - inst_081: Pluralism - different value frameworks are equally legitimate + * - Detect Western-centric framings inappropriate for other cultures + * - FLAG for review, never auto-block (human decides) + * - Suggest adaptations, but preserve human agency + */ + async assessCulturalSensitivity(content, context = {}) { + const { + audience = {}, + content_type = 'general', + target_publications = [] + } = context; + + logger.info('[PluralisticDeliberationOrchestrator] Assessing cultural sensitivity', { + content_type, + audience_country: audience.country, + audience_region: audience.region + }); + + const assessment = { + culturally_sensitive: true, + risk_level: 'LOW', + concerns: [], + suggestions: [], + recommended_action: 'APPROVE', + timestamp: new Date(), + context + }; + + // Check if audience is non-Western or culturally distinct + const highRiskAudience = this._identifyHighRiskAudience(audience, target_publications); + const sensitiveTopics = this._detectSensitiveTopics(content); + + // Western-centric governance language + const westernGovernancePatterns = { + democracy: { + patterns: [/\bdemocrac(?:y|tic)\b/gi, /\bdemocratic\s+(?:governance|oversight|control)\b/gi], + concern: 'Democratic framing may have political connotations in autocratic contexts', + suggestion: 'Consider "participatory governance", "stakeholder input", or "inclusive decision-making"' + }, + individual_rights: { + patterns: [/\bindividual\s+(?:rights|freedom|autonomy)\b/gi, /\bpersonal\s+freedom\b/gi], + concern: 'Individualistic framing may not resonate in collectivist cultures', + suggestion: 'Balance with "community wellbeing", "collective benefit", or "shared responsibility"' + }, + western_ethics_only: { + patterns: [/\bethics\b(?!.*(?:diverse|pluralistic|multiple|indigenous))/gi], + concern: 'Implies universal Western ethics without acknowledging other frameworks', + suggestion: 'Reference "diverse ethical frameworks" or "culturally-grounded values"' + }, + freedom_emphasis: { + patterns: [/\bfreedom\s+of\s+(?:speech|expression|press)\b/gi], + concern: 'Western rights discourse may be politically sensitive in some regions', + suggestion: 'Frame as "open communication" or "information access" if contextually appropriate' + } + }; + + // Indigenous/cultural insensitivity patterns + const culturalInsensitivityPatterns = { + western_only_governance: { + patterns: [/\bgovernance\b(?!.*(?:indigenous|te\s+tiriti|care\s+principles))/gi], + concern: 'Western governance frameworks only, excludes Indigenous perspectives', + suggestion: 'Acknowledge Indigenous governance (Te Tiriti, CARE principles, relational sovereignty)' + }, + data_ownership_western: { + patterns: [/\bdata\s+(?:ownership|rights)\b(?!.*(?:sovereignty|collective))/gi], + concern: 'Individual data ownership framing conflicts with collective Indigenous data sovereignty', + suggestion: 'Reference "data sovereignty", "collective data rights", or "community data governance"' + } + }; + + // Check patterns + if (highRiskAudience || sensitiveTopics.length > 0) { + // Scan for Western-centric patterns + for (const [key, config] of Object.entries(westernGovernancePatterns)) { + for (const pattern of config.patterns) { + if (pattern.test(content)) { + assessment.culturally_sensitive = false; + assessment.concerns.push({ + type: 'western_centric_framing', + pattern_key: key, + detail: config.concern, + audience_context: this._formatAudienceContext(audience, target_publications) + }); + assessment.suggestions.push({ + type: 'reframing', + original_concern: key, + suggestion: config.suggestion + }); + } + } + } + + // Scan for Indigenous insensitivity (if audience includes Indigenous communities) + if (this._isIndigenousAudience(audience, target_publications)) { + for (const [key, config] of Object.entries(culturalInsensitivityPatterns)) { + for (const pattern of config.patterns) { + if (pattern.test(content)) { + assessment.culturally_sensitive = false; + assessment.concerns.push({ + type: 'indigenous_exclusion', + pattern_key: key, + detail: config.concern, + audience_context: 'Indigenous community or Te Tiriti context' + }); + assessment.suggestions.push({ + type: 'indigenous_inclusion', + original_concern: key, + suggestion: config.suggestion + }); + } + } + } + } + } + + // Determine risk level and recommended action + const highSeverityConcerns = assessment.concerns.filter(c => + c.type === 'western_centric_framing' && highRiskAudience + ); + + if (assessment.concerns.length === 0) { + assessment.risk_level = 'LOW'; + assessment.recommended_action = 'APPROVE'; + } else if (highSeverityConcerns.length > 0 || assessment.concerns.length >= 3) { + assessment.risk_level = 'HIGH'; + assessment.recommended_action = 'HUMAN_REVIEW'; + } else { + assessment.risk_level = 'MEDIUM'; + assessment.recommended_action = 'SUGGEST_ADAPTATION'; + } + + // Audit log + this._auditCulturalSensitivity(assessment).catch(error => { + logger.error('[PluralisticDeliberationOrchestrator] Failed to audit cultural sensitivity check', { + error: error.message + }); + }); + + logger.info('[PluralisticDeliberationOrchestrator] Cultural sensitivity assessment complete', { + risk_level: assessment.risk_level, + concerns_count: assessment.concerns.length, + recommended_action: assessment.recommended_action + }); + + return assessment; + } + + /** + * Identify if audience is high-risk for cultural insensitivity + * @private + */ + _identifyHighRiskAudience(audience, target_publications = []) { + // Non-Western countries + const nonWesternCountries = ['CN', 'RU', 'SA', 'IR', 'VN', 'TH', 'ID', 'MY', 'PH']; + if (audience.country && nonWesternCountries.includes(audience.country)) { + return true; + } + + // Indigenous communities + if (audience.cultural_context && audience.cultural_context.includes('indigenous')) { + return true; + } + + // Publications in non-Western regions + if (target_publications.some(pub => pub.region && !['Western', 'US', 'EU', 'NZ', 'AU', 'CA'].includes(pub.region))) { + return true; + } + + return false; + } + + /** + * Detect sensitive topics in content + * @private + */ + _detectSensitiveTopics(content) { + const topics = []; + const topicPatterns = { + values: /\b(?:values|ethics|morals?|principles)\b/gi, + governance: /\b(?:governance|democracy|rights|freedom)\b/gi, + religion: /\b(?:religion|spiritual|faith|belief)\b/gi, + politics: /\b(?:political|government|state|regime)\b/gi, + culture: /\b(?:culture|tradition|indigenous|tribal)\b/gi + }; + + for (const [topic, pattern] of Object.entries(topicPatterns)) { + if (pattern.test(content)) { + topics.push(topic); + } + } + + return topics; + } + + /** + * Check if audience is Indigenous + * @private + */ + _isIndigenousAudience(audience, target_publications = []) { + if (audience.cultural_context && /indigenous|māori|aboriginal|first\s+nations/i.test(audience.cultural_context)) { + return true; + } + + if (target_publications.some(pub => pub.audience && /indigenous|māori/i.test(pub.audience))) { + return true; + } + + return false; + } + + /** + * Format audience context for reporting + * @private + */ + _formatAudienceContext(audience, target_publications) { + const parts = []; + if (audience.country) parts.push(`Country: ${audience.country}`); + if (audience.region) parts.push(`Region: ${audience.region}`); + if (audience.outlet) parts.push(`Outlet: ${audience.outlet}`); + if (target_publications.length > 0) { + parts.push(`Publications: ${target_publications.map(p => p.name).join(', ')}`); + } + return parts.join(' | ') || 'General audience'; + } + + /** + * Audit cultural sensitivity check + * @private + */ + async _auditCulturalSensitivity(assessment) { + try { + const memoryProxy = await getMemoryProxy(); + const collection = await memoryProxy.getCollection('auditLogs'); + + await collection.insertOne({ + timestamp: new Date(), + service: 'PluralisticDeliberationOrchestrator', + action: 'cultural_sensitivity_check', + decision: assessment.recommended_action, + context: { + content_type: assessment.context.content_type, + audience: assessment.context.audience, + risk_level: assessment.risk_level, + concerns_count: assessment.concerns.length, + culturally_sensitive: assessment.culturally_sensitive + }, + metadata: { + concerns: assessment.concerns, + suggestions: assessment.suggestions + } + }); + } catch (error) { + logger.error('[PluralisticDeliberationOrchestrator] Failed to create audit log', { error: error.message }); + } + } + /** * Get deliberation statistics * @returns {Object} Statistics object