/** * Content Governance Checker Service * Unified framework checker for all external communications * * Used by: * - Blog posts (before publication) * - Media inquiry responses (before sending) * - Response templates (on create/update) * - Newsletter content (future) * * Enforces: * - inst_016: No fabricated statistics without citation * - inst_017: No absolute guarantees * - inst_018: No unverified production claims * - inst_079: No dark patterns or manipulative urgency */ // Pattern definitions from governance rules const contentPatterns = { inst_016_fabricated_stats: { patterns: [ /\b\d+%\s+(?:faster|better|improvement|increase|decrease|reduction|more|less)\b(?!\s*\[NEEDS VERIFICATION\]|\s*\(source:|\s*\[source:)/gi, /\b(?:faster|better|improvement)\s+of\s+\d+%\b(?!\s*\[NEEDS VERIFICATION\]|\s*\(source:|\s*\[source:)/gi, /\b\d+x\s+(?:faster|better|more|increase)\b(?!\s*\[NEEDS VERIFICATION\]|\s*\(source:|\s*\[source:)/gi ], severity: 'HIGH', message: 'Statistics require citation or [NEEDS VERIFICATION] marker (inst_016)', instruction: 'inst_016' }, inst_017_absolute_guarantees: { patterns: [ /\bguarantee(?:s|d|ing)?\b/gi, /\b100%\s+(?:secure|safe|reliable|effective)\b/gi, /\bcompletely\s+prevents?\b/gi, /\bnever\s+fails?\b/gi, /\balways\s+works?\b/gi, /\beliminates?\s+all\b/gi, /\bperfect(?:ly)?\s+(?:secure|safe|reliable)\b/gi ], severity: 'HIGH', message: 'Absolute guarantees prohibited - use evidence-based language (inst_017)', instruction: 'inst_017' }, inst_018_unverified_claims: { patterns: [ /\bproduction-ready\b(?!\s+development\s+tool|\s+proof-of-concept)/gi, /\bbattle-tested\b/gi, /\benterprise-proven\b/gi, /\bwidespread\s+adoption\b/gi, /\bindustry-standard\b/gi ], severity: 'MEDIUM', message: 'Production claims require evidence (this is proof-of-concept) (inst_018)', instruction: 'inst_018' }, inst_079_dark_patterns: { patterns: [ /\bclick\s+here\s+now\b/gi, /\blimited\s+time\s+offer\b/gi, /\bonly\s+\d+\s+spots?\s+left\b/gi, /\bact\s+fast\b/gi, /\bhurry\b.*\b(?:before|while)\b/gi, /\bdon't\s+miss\s+out\b/gi ], severity: 'MEDIUM', message: 'Possible manipulative urgency/dark pattern detected (inst_079)', instruction: 'inst_079' } }; /** * Scan content for governance violations * @param {string} text - Content to scan * @param {object} options - Scanning options * @returns {Promise} - Scan results with violations array */ async function scanContent(text, options = {}) { const { type = 'general', // blog, media_response, template, newsletter context = {} } = options; if (!text || typeof text !== 'string') { return { success: true, violations: [], scannedAt: new Date(), type, context }; } const violations = []; // Scan for each pattern for (const [ruleId, config] of Object.entries(contentPatterns)) { for (const pattern of config.patterns) { const matches = text.match(pattern); if (matches) { // Get unique matches only const uniqueMatches = [...new Set(matches)]; uniqueMatches.forEach(match => { violations.push({ rule: ruleId, instruction: config.instruction, severity: config.severity, match, message: config.message, position: text.indexOf(match), context: getMatchContext(text, text.indexOf(match)) }); }); } } } return { success: violations.length === 0, violations, scannedAt: new Date(), type, context, summary: { total: violations.length, high: violations.filter(v => v.severity === 'HIGH').length, medium: violations.filter(v => v.severity === 'MEDIUM').length, byRule: violations.reduce((acc, v) => { acc[v.instruction] = (acc[v.instruction] || 0) + 1; return acc; }, {}) } }; } /** * Get context around a match (50 chars before and after) */ function getMatchContext(text, position, contextLength = 50) { const start = Math.max(0, position - contextLength); const end = Math.min(text.length, position + contextLength); let context = text.substring(start, end); // Add ellipsis if truncated if (start > 0) context = '...' + context; if (end < text.length) context = context + '...'; return context; } /** * Format violations for display */ function formatViolations(violations) { if (!violations || violations.length === 0) { return 'No violations found'; } return violations.map((v, idx) => `${idx + 1}. [${v.severity}] ${v.rule}\n` + ` Match: "${v.match}"\n` + ` ${v.message}\n` + ` Context: ${v.context}` ).join('\n\n'); } /** * Check if content is safe for publication * Convenience method that returns boolean */ async function isSafeForPublication(text, options = {}) { const result = await scanContent(text, options); return result.success; } module.exports = { scanContent, formatViolations, isSafeForPublication, contentPatterns };