feat(cultural-sensitivity): implement Phase 1 - detection and flagging (inst_081)
Phase 1: Cultural Sensitivity Detection Layer - Detects Western-centric framing (democracy, individual rights, freedom) - Detects Indigenous exclusion (missing Te Tiriti, CARE principles) - FLAGS for human review, never auto-blocks (preserves human agency) Implementation: - PluralisticDeliberationOrchestrator.assessCulturalSensitivity() - Pattern-based detection (Western-centric governance, Indigenous exclusion) - Risk levels: LOW, MEDIUM, HIGH - Recommended actions: APPROVE, SUGGEST_ADAPTATION, HUMAN_REVIEW - High-risk audiences: Non-Western countries (CN, RU, SA, IR, VN, TH, ID, MY, PH), Indigenous communities - Audit logging to MongoDB - media.controller.js respondToInquiry() - Cultural check after ContentGovernanceChecker passes - Stores cultural_sensitivity in response metadata - Returns flag if HIGH risk (doesn't block, flags for review) - blog.controller.js publishPost() - Cultural check after framework governance check - Stores cultural_sensitivity in moderation.cultural_sensitivity - Returns flag if HIGH risk (doesn't block, flags for review) - MediaInquiry.model.js - Added country, cultural_context fields to contact - respond() method supports cultural_sensitivity in response metadata Framework Integration: - Dual-layer governance: Universal rules (ContentGovernanceChecker) + Cultural sensitivity (PluralisticDeliberationOrchestrator) - inst_081 pluralism: Different value frameworks equally legitimate - Human-in-the-loop: AI detects/suggests, human decides Next: Phase 2 (UI/workflow), Phase 3 (learning/refinement) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cf09b66c32
commit
b19f6de5c8
4 changed files with 430 additions and 18 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Object>} 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue