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:
TheFlow 2025-10-25 11:10:06 +13:00
parent 8217f3cb8c
commit cd97a5384d
4 changed files with 430 additions and 18 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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 result = await collection.updateOne(
{ _id: new ObjectId(id) },
{
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) },
updateDoc
);
return result.modifiedCount > 0;

View file

@ -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