feat(framework): implement Phase 1 proactive content scanning

CREATED:
- scripts/framework-components/ProhibitedTermsScanner.js (420 lines)
  • Scans codebase for inst_016/017/018 violations
  • Pattern detection for guarantee language, fabricated stats, unverified claims
  • Auto-fix capability with context awareness
  • CLI interface: --details, --fix, --staged flags

- tests/unit/ProhibitedTermsScanner.test.js (39 tests, all passing)
  • Pattern detection tests (inst_017, inst_018)
  • Context awareness tests
  • Auto-fix functionality tests
  • Edge case handling

MODIFIED:
- scripts/session-init.js
  • Added Section 7: Scanning for Prohibited Terms
  • Renumbered subsequent sections (CSP → 8, Dev Env → 9, Continuous → 10)
  • Scans on every session start, reports violations

- scripts/hook-validators/validate-file-write.js
  • Added missing checkPreActionCheckRecency() function (fixes hook crash)

- package.json/package-lock.json
  • Added glob@11.0.3 dependency

RESULTS:
• Scanner operational: 39/39 tests passing
• Session integration: Runs automatically on session start
• Current scan: Found 364 violations (188 inst_017, 120 inst_018, 56 inst_016)
• Violations need user review (many in historical docs, specifications)

IMPACT:
• Framework now PROACTIVE instead of reactive
• Violations detected at session start (not weeks later)
• Auto-fix available for simple cases
• Closes critical detection gap identified in framework assessment

NEXT STEPS (user decision):
• Review 364 violations (many false positives in historical docs)
• Optionally: Implement pre-commit hook
• Phase 2: Context-aware rule surfacing
• Phase 3: Active metacognitive assistance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-21 17:37:51 +13:00
parent 6a80f344c1
commit 1fe50500f0
4 changed files with 973 additions and 3 deletions

View file

@ -0,0 +1,440 @@
/**
* ProhibitedTermsScanner
*
* Proactively scans codebase for violations of inst_016/017/018
* Part of Framework Improvement Phase 1: Proactive Content Scanning
*
* Usage:
* const scanner = new ProhibitedTermsScanner();
* const violations = await scanner.scan();
* const fixed = await scanner.autoFix(violations);
*
* CLI:
* node scripts/framework-components/ProhibitedTermsScanner.js [--details] [--fix] [--staged]
*/
const fs = require('fs').promises;
const path = require('path');
const { glob } = require('glob');
const { execSync } = require('child_process');
class ProhibitedTermsScanner {
constructor(options = {}) {
this.options = {
silent: options.silent || false,
fixMode: options.fixMode || false,
staged: options.staged || false,
basePath: options.basePath || process.cwd(),
...options
};
// Pattern definitions from inst_016/017/018
this.patterns = [
{
id: 'inst_017',
name: 'Absolute Assurance Terms',
severity: 'HIGH',
patterns: [
/\bguarantee(?:s|d|ing)?\b/gi,
/ensures?\s+100%/gi,
/eliminates?\s+all\b/gi,
/completely\s+prevents?\b/gi,
/never\s+fails?\b/gi,
/always\s+works?\b/gi
],
suggestions: {
'guarantee': 'enforcement',
'guarantees': 'enforces',
'guaranteed': 'enforced',
'guaranteeing': 'enforcing',
'ensures 100%': 'helps ensure',
'ensure 100%': 'help ensure',
'eliminates all': 'reduces',
'eliminate all': 'reduce',
'completely prevents': 'designed to prevent',
'completely prevent': 'designed to prevent',
'never fails': 'designed to prevent failures',
'never fail': 'designed to prevent failures',
'always works': 'designed to work',
'always work': 'designed to work'
}
},
{
id: 'inst_016',
name: 'Fabricated Statistics',
severity: 'HIGH',
patterns: [
// Match percentage claims without [NEEDS VERIFICATION] or source citations
/\b\d+%\s+(?:faster|better|improvement|increase|decrease|reduction|more|less)\b(?!\s*\[NEEDS VERIFICATION\]|\s*\(source:|\s*\[source:)/gi,
/\b(?:faster|better|improvement|increase|decrease|reduction)\s+of\s+\d+%\b(?!\s*\[NEEDS VERIFICATION\]|\s*\(source:|\s*\[source:)/gi
],
suggestions: {
'default': 'Add [NEEDS VERIFICATION] or cite source'
}
},
{
id: 'inst_018',
name: 'Unverified Readiness Claims',
severity: 'MEDIUM',
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,
/\bcustomer\s+base\b(?!\s+of\s+zero|\s+\(none\))/gi,
/\bmarket\s+validation\b(?!\s+pending|\s+not\s+yet)/gi
],
suggestions: {
'production-ready': 'proof-of-concept',
'battle-tested': 'in development',
'enterprise-proven': 'designed for',
'widespread adoption': 'early development',
'customer base': 'development project',
'market validation': 'internal validation'
}
}
];
// File inclusion patterns
this.includePatterns = [
'**/*.md',
'**/*.html',
'**/*.js',
'**/*.json',
'**/*.jsx',
'**/*.tsx'
];
// File exclusion patterns
this.excludePatterns = [
'**/node_modules/**',
'**/.git/**',
'**/.claude/**',
'**/tests/**/*.test.js',
'**/tests/**/*.spec.js',
'**/docs/case-studies/**',
'**/GOVERNANCE-RULE-LIBRARY.md',
'**/.claude/instruction-history.json',
'**/dist/**',
'**/build/**',
'**/.next/**'
];
}
/**
* Scan files for prohibited terms
* @param {Object} options - Scan options
* @returns {Promise<Array>} Array of violations
*/
async scan(options = {}) {
const scanOptions = { ...this.options, ...options };
const violations = [];
// Get files to scan
const files = await this.getFilesToScan(scanOptions.staged);
if (!scanOptions.silent) {
console.log(`\n🔍 Scanning ${files.length} files for prohibited terms...`);
}
// Scan each file
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf8');
const lines = content.split('\n');
// Check each pattern type
for (const patternSet of this.patterns) {
for (const pattern of patternSet.patterns) {
lines.forEach((line, index) => {
const matches = line.match(pattern);
if (matches) {
matches.forEach(match => {
// Skip if in allowed context
if (this.isAllowedContext(line, match, file)) {
return;
}
violations.push({
file,
line: index + 1,
match,
rule: patternSet.id,
ruleName: patternSet.name,
severity: patternSet.severity,
context: line.trim(),
suggestion: this.getSuggestion(match, patternSet.suggestions)
});
});
}
});
}
}
} catch (err) {
// Skip files that can't be read (binary files, etc.)
if (err.code !== 'ENOENT') {
console.error(`⚠ Error reading ${file}: ${err.message}`);
}
}
}
return violations;
}
/**
* Auto-fix simple violations
* @param {Array} violations - Violations to fix
* @returns {Promise<Object>} Fix results
*/
async autoFix(violations) {
const results = {
fixed: 0,
total: violations.length,
skipped: 0,
errors: []
};
// Group violations by file
const fileGroups = violations.reduce((acc, v) => {
if (!acc[v.file]) acc[v.file] = [];
acc[v.file].push(v);
return acc;
}, {});
// Fix each file
for (const [file, fileViolations] of Object.entries(fileGroups)) {
try {
let content = await fs.readFile(file, 'utf8');
let modified = false;
// Apply fixes (reverse order to preserve line numbers)
for (const violation of fileViolations.reverse()) {
// Only auto-fix if we have a clear suggestion
if (violation.suggestion && violation.suggestion !== 'Add [NEEDS VERIFICATION] or cite source') {
const originalContent = content;
// Simple case-preserving replacement
const regex = new RegExp(this.escapeRegex(violation.match), 'g');
content = content.replace(regex, violation.suggestion);
if (content !== originalContent) {
modified = true;
results.fixed++;
}
} else {
results.skipped++;
}
}
// Write file if modified
if (modified) {
await fs.writeFile(file, content, 'utf8');
console.log(`✓ Fixed ${file}`);
}
} catch (err) {
results.errors.push({ file, error: err.message });
console.error(`✗ Error fixing ${file}: ${err.message}`);
}
}
return results;
}
/**
* Get files to scan
* @param {boolean} stagedOnly - Only scan staged files
* @returns {Promise<Array>} Array of file paths
*/
async getFilesToScan(stagedOnly = false) {
if (stagedOnly) {
try {
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
return output.split('\n').filter(f => f.trim());
} catch (err) {
console.error('⚠ Error getting staged files, falling back to all files');
}
}
// Use glob to find all matching files
const files = [];
for (const pattern of this.includePatterns) {
try {
const matches = await glob(pattern, {
ignore: this.excludePatterns,
nodir: true,
cwd: this.options.basePath
});
// glob returns an array, so we can spread it
if (Array.isArray(matches)) {
// Prepend base path to make absolute paths
const absolutePaths = matches.map(f => path.join(this.options.basePath, f));
files.push(...absolutePaths);
}
} catch (err) {
// Ignore glob errors (e.g., pattern doesn't match anything)
}
}
// Remove duplicates
return [...new Set(files)];
}
/**
* Check if context allows the term
* @param {string} line - Line containing match
* @param {string} match - Matched term
* @param {string} file - File path
* @returns {boolean} True if allowed
*/
isAllowedContext(line, match, file) {
// Allow in comments about the rules themselves
if (line.includes('inst_017') || line.includes('inst_016') || line.includes('inst_018')) {
return true;
}
// Allow in GOVERNANCE-RULE-LIBRARY.md
if (file.includes('GOVERNANCE-RULE-LIBRARY.md')) {
return true;
}
// Allow in case studies
if (file.includes('case-studies')) {
return true;
}
// Allow in test files (shouldn't reach here but double-check)
if (file.includes('.test.') || file.includes('.spec.')) {
return true;
}
// Allow "production-ready development tool" or "production-ready proof-of-concept"
if (match.toLowerCase() === 'production-ready') {
if (line.includes('development tool') || line.includes('proof-of-concept')) {
return true;
}
}
return false;
}
/**
* Get suggestion for a match
* @param {string} match - Matched term
* @param {Object} suggestions - Suggestion map
* @returns {string} Suggestion
*/
getSuggestion(match, suggestions) {
const lowerMatch = match.toLowerCase();
// Try exact match first
if (suggestions[lowerMatch]) {
return suggestions[lowerMatch];
}
// Try partial matches
for (const [key, value] of Object.entries(suggestions)) {
if (lowerMatch.includes(key)) {
return value;
}
}
return suggestions.default || 'Review and revise';
}
/**
* Format violations for display
* @param {Array} violations - Violations to format
* @param {boolean} detailed - Show detailed output
* @returns {string} Formatted output
*/
formatViolations(violations, detailed = false) {
if (violations.length === 0) {
return '\n✅ No prohibited terms found\n';
}
// Group by rule
const byRule = violations.reduce((acc, v) => {
if (!acc[v.rule]) acc[v.rule] = [];
acc[v.rule].push(v);
return acc;
}, {});
let output = `\n⚠ Found ${violations.length} violation(s):\n`;
// Summary
for (const [rule, items] of Object.entries(byRule)) {
output += ` ${rule}: ${items.length} violation(s)\n`;
}
// Details
if (detailed) {
output += '\nDetails:\n';
for (const v of violations) {
output += `\n ${v.file}:${v.line}\n`;
output += ` Rule: ${v.rule} (${v.severity})\n`;
output += ` Found: "${v.match}"\n`;
output += ` Context: ${v.context.substring(0, 80)}...\n`;
output += ` Suggestion: ${v.suggestion}\n`;
}
} else {
output += '\nRun with --details for full violation list\n';
}
output += '\nTo fix: node scripts/framework-components/ProhibitedTermsScanner.js --fix\n';
return output;
}
/**
* Escape regex special characters
* @param {string} str - String to escape
* @returns {string} Escaped string
*/
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
// CLI interface
async function main() {
const args = process.argv.slice(2);
const options = {
silent: false,
fixMode: args.includes('--fix'),
staged: args.includes('--staged'),
details: args.includes('--details')
};
const scanner = new ProhibitedTermsScanner(options);
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log(' Tractatus Framework - Prohibited Terms Scanner');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
const violations = await scanner.scan();
console.log(scanner.formatViolations(violations, options.details));
if (options.fixMode && violations.length > 0) {
console.log('\n🔧 Applying auto-fixes...\n');
const results = await scanner.autoFix(violations);
console.log(`\n✓ Fixed: ${results.fixed}`);
console.log(`⊘ Skipped: ${results.skipped} (manual review required)`);
if (results.errors.length > 0) {
console.log(`✗ Errors: ${results.errors.length}`);
}
}
// Exit with error code if violations found (for pre-commit hooks)
process.exit(violations.length > 0 ? 1 : 0);
}
// Run if called directly
if (require.main === module) {
main().catch(err => {
console.error('Error:', err);
process.exit(1);
});
}
module.exports = ProhibitedTermsScanner;

View file

@ -149,6 +149,42 @@ function checkCSPComplianceOnNewContent() {
}
/**
* Check 1b: Pre-action-check recency (inst_038)
*/
function checkPreActionCheckRecency() {
// Only enforce for major file changes
// Check if pre-action-check was run recently
try {
if (!fs.existsSync(SESSION_STATE_PATH)) {
// No session state, allow but warn
warning('No session state - pre-action-check recommended (inst_038)');
return { passed: true };
}
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
const lastPreActionCheck = sessionState.last_pre_action_check;
const currentAction = sessionState.action_count || 0;
// If never run, just warn (don't block on first action)
if (!lastPreActionCheck) {
warning('inst_038: Pre-action-check not run in this session');
warning('Recommended: node scripts/pre-action-check.js write ' + FILE_PATH);
return { passed: true }; // Warning only
}
// If last run was more than 10 actions ago, warn
if (currentAction - lastPreActionCheck > 10) {
warning(`inst_038: Pre-action-check last run ${currentAction - lastPreActionCheck} actions ago`);
warning('Consider running: node scripts/pre-action-check.js write ' + FILE_PATH);
}
return { passed: true };
} catch (error) {
warning(`Could not check pre-action-check recency: ${error.message}`);
return { passed: true };
}
}
/**
* Check 2: Overwrite without read

View file

@ -384,8 +384,41 @@ async function main() {
process.exit(1); // Exit with failure code
}
// Prohibited Terms Scan (Framework Phase 1)
section('7. Scanning for Prohibited Terms');
try {
const ProhibitedTermsScanner = require('./framework-components/ProhibitedTermsScanner');
const scanner = new ProhibitedTermsScanner({ silent: false });
const violations = await scanner.scan();
if (violations.length === 0) {
success('No prohibited terms found (inst_016/017/018 compliant)');
} else {
console.log('');
warning(`Found ${violations.length} violation(s) in user-facing content:`);
// Group by rule
const byRule = violations.reduce((acc, v) => {
if (!acc[v.rule]) acc[v.rule] = [];
acc[v.rule].push(v);
return acc;
}, {});
Object.entries(byRule).forEach(([rule, items]) => {
log(` ${rule}: ${items.length} violation(s)`, 'yellow');
});
console.log('');
log(' Run: node scripts/framework-components/ProhibitedTermsScanner.js --details', 'cyan');
log(' Or: node scripts/framework-components/ProhibitedTermsScanner.js --fix', 'cyan');
console.log('');
}
} catch (err) {
warning(`Could not run prohibited terms scanner: ${err.message}`);
}
// CSP Compliance Scan
section('7. CSP Compliance Scan (inst_008)');
section('8. CSP Compliance Scan (inst_008)');
try {
const { scanForViolations, displayViolations } = require('./check-csp-violations');
const violations = scanForViolations();
@ -416,7 +449,7 @@ async function main() {
}
// ENFORCEMENT: Local development server check
section('8. Development Environment Enforcement');
section('9. Development Environment Enforcement');
const localServerRunning = checkLocalServer();
if (!localServerRunning) {
@ -447,7 +480,7 @@ async function main() {
success('Development environment ready');
// Hook Architecture Status
section('9. Continuous Enforcement Architecture');
section('10. Continuous Enforcement Architecture');
const hookValidatorsExist = fs.existsSync(path.join(__dirname, 'hook-validators'));
if (hookValidatorsExist) {

View file

@ -0,0 +1,461 @@
/**
* Unit Tests - ProhibitedTermsScanner
* Tests for proactive content scanning (Framework Phase 1)
*/
const fs = require('fs').promises;
const path = require('path');
const ProhibitedTermsScanner = require('../../scripts/framework-components/ProhibitedTermsScanner');
describe('ProhibitedTermsScanner', () => {
let scanner;
const testFilesDir = path.join(__dirname, '../tmp-scanner-test');
beforeEach(() => {
scanner = new ProhibitedTermsScanner({
silent: true,
basePath: testFilesDir // Only scan test directory
});
});
afterEach(async () => {
// Clean up test files
try {
await fs.rm(testFilesDir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors
}
});
describe('Pattern Detection - inst_017 (Absolute Assurance)', () => {
test('should detect "guarantee" variations', async () => {
const testContent = `
This guarantees safety.
We guarantee results.
This is guaranteed to work.
We are guaranteeing success.
`;
// Create test file
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const inst017Violations = violations.filter(v => v.rule === 'inst_017');
expect(inst017Violations.length).toBeGreaterThanOrEqual(4);
expect(inst017Violations.some(v => v.match.toLowerCase().includes('guarantee'))).toBe(true);
});
test('should detect "ensures 100%"', async () => {
const testContent = 'This ensures 100% accuracy.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.html');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const inst017Violations = violations.filter(v => v.rule === 'inst_017');
expect(inst017Violations.length).toBeGreaterThan(0);
expect(inst017Violations.some(v => v.match.toLowerCase().includes('ensures'))).toBe(true);
});
test('should detect "eliminates all"', async () => {
const testContent = 'This eliminates all bugs.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.js');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
expect(violations.length).toBeGreaterThan(0);
expect(violations.some(v => v.match.toLowerCase().includes('eliminates'))).toBe(true);
});
test('should detect "never fails"', async () => {
const testContent = 'This system never fails.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
expect(violations.length).toBeGreaterThan(0);
expect(violations.some(v => v.match.toLowerCase().includes('never'))).toBe(true);
});
});
describe('Pattern Detection - inst_018 (Unverified Claims)', () => {
test('should detect "production-ready" without context', async () => {
const testContent = 'This is production-ready software.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const inst018Violations = violations.filter(v => v.rule === 'inst_018');
expect(inst018Violations.length).toBeGreaterThan(0);
});
test('should detect "battle-tested"', async () => {
const testContent = 'Our battle-tested framework.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.html');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
expect(violations.some(v => v.match.toLowerCase().includes('battle-tested'))).toBe(true);
});
test('should allow "production-ready development tool"', async () => {
const testContent = 'Tractatus is a production-ready development tool.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const inst018Violations = violations.filter(v =>
v.rule === 'inst_018' && v.match.toLowerCase().includes('production-ready')
);
expect(inst018Violations.length).toBe(0);
});
test('should allow "production-ready proof-of-concept"', async () => {
const testContent = 'This is a production-ready proof-of-concept.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const inst018Violations = violations.filter(v =>
v.rule === 'inst_018' && v.match.toLowerCase().includes('production-ready')
);
expect(inst018Violations.length).toBe(0);
});
});
describe('Context Awareness', () => {
test('should allow prohibited terms in comments about rules', async () => {
const testContent = `
// inst_017: Never use "guarantee"
// inst_017 prohibits guaranteed language
`;
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.js');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
// Should not flag violations in comments about the rules
expect(violations.length).toBe(0);
});
test('should exclude test files from scanning', async () => {
// Test files should be excluded by pattern
const scanner2 = new ProhibitedTermsScanner({ silent: true });
expect(scanner2.excludePatterns).toContain('**/tests/**/*.test.js');
expect(scanner2.excludePatterns).toContain('**/tests/**/*.spec.js');
});
test('should exclude GOVERNANCE-RULE-LIBRARY.md', async () => {
expect(scanner.excludePatterns).toContain('**/GOVERNANCE-RULE-LIBRARY.md');
});
test('should exclude case studies', async () => {
expect(scanner.excludePatterns).toContain('**/docs/case-studies/**');
});
});
describe('Suggestions', () => {
test('should suggest "enforcement" for "guarantee"', () => {
const suggestions = scanner.patterns.find(p => p.id === 'inst_017').suggestions;
expect(suggestions['guarantee']).toBe('enforcement');
expect(suggestions['guarantees']).toBe('enforces');
expect(suggestions['guaranteed']).toBe('enforced');
});
test('should suggest replacements for "ensures 100%"', () => {
const suggestions = scanner.patterns.find(p => p.id === 'inst_017').suggestions;
expect(suggestions['ensures 100%']).toBe('helps ensure');
});
test('should suggest replacements for inst_018 terms', () => {
const suggestions = scanner.patterns.find(p => p.id === 'inst_018').suggestions;
expect(suggestions['production-ready']).toBe('proof-of-concept');
expect(suggestions['battle-tested']).toBe('in development');
});
test('should get suggestion for matched term', () => {
const patternSet = scanner.patterns.find(p => p.id === 'inst_017');
const suggestion = scanner.getSuggestion('guarantee', patternSet.suggestions);
expect(suggestion).toBe('enforcement');
});
test('should handle case-insensitive matches', () => {
const patternSet = scanner.patterns.find(p => p.id === 'inst_017');
const suggestion = scanner.getSuggestion('GUARANTEE', patternSet.suggestions);
expect(suggestion).toBe('enforcement');
});
});
describe('Auto-fix Functionality', () => {
test('should fix simple violations', async () => {
const testContent = 'This guarantees safety.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const results = await scanner.autoFix(violations);
expect(results.fixed).toBeGreaterThan(0);
const fixedContent = await fs.readFile(testFile, 'utf8');
expect(fixedContent).toContain('enforces'); // guarantees → enforces
expect(fixedContent).not.toContain('guarantees');
});
test('should preserve file structure during fix', async () => {
const testContent = `Line 1: Normal content
Line 2: This guarantees results
Line 3: More content
Line 4: This guaranteed safety
Line 5: Final line`;
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
await scanner.autoFix(violations);
const fixedContent = await fs.readFile(testFile, 'utf8');
const lines = fixedContent.split('\n');
expect(lines.length).toBe(5);
expect(lines[0]).toBe('Line 1: Normal content');
expect(lines[4]).toBe('Line 5: Final line');
});
test('should handle multiple violations in same file', async () => {
const testContent = `
We guarantee success.
This is guaranteed to work.
Our guarantees are strong.
`;
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const results = await scanner.autoFix(violations);
expect(results.fixed).toBeGreaterThan(0);
const fixedContent = await fs.readFile(testFile, 'utf8');
expect(fixedContent).not.toContain('guarantee');
});
test('should skip violations without clear suggestions', async () => {
// inst_016 violations (fabricated statistics) require manual review
const testContent = '95% faster performance.';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
const inst016Violations = violations.filter(v => v.rule === 'inst_016');
const results = await scanner.autoFix(inst016Violations);
// inst_016 requires source citation, can't auto-fix
expect(results.skipped).toBeGreaterThan(0);
});
});
describe('Formatting', () => {
test('should format empty violations list', () => {
const output = scanner.formatViolations([]);
expect(output).toContain('No prohibited terms found');
});
test('should format violations summary', () => {
const violations = [
{ rule: 'inst_017', file: 'test.md', line: 1, match: 'guarantee', severity: 'HIGH' },
{ rule: 'inst_017', file: 'test.md', line: 2, match: 'guaranteed', severity: 'HIGH' },
{ rule: 'inst_018', file: 'test.html', line: 5, match: 'battle-tested', severity: 'MEDIUM' }
];
const output = scanner.formatViolations(violations, false);
expect(output).toContain('Found 3 violation');
expect(output).toContain('inst_017: 2');
expect(output).toContain('inst_018: 1');
});
test('should format detailed violations', () => {
const violations = [
{
rule: 'inst_017',
file: 'test.md',
line: 1,
match: 'guarantee',
severity: 'HIGH',
context: 'This guarantees safety',
suggestion: 'enforcement'
}
];
const output = scanner.formatViolations(violations, true);
expect(output).toContain('test.md:1');
expect(output).toContain('Found: "guarantee"');
expect(output).toContain('Suggestion: enforcement');
});
});
describe('Utility Functions', () => {
test('should escape regex special characters', () => {
const escaped = scanner.escapeRegex('test.file[0]');
expect(escaped).toBe('test\\.file\\[0\\]');
});
test('should check allowed context for inst_017 references', () => {
const line = 'inst_017 prohibits guaranteed language';
const result = scanner.isAllowedContext(line, 'guaranteed', 'test.md');
expect(result).toBe(true);
});
test('should check allowed context for case studies', () => {
const line = 'This guarantees results';
const result = scanner.isAllowedContext(line, 'guarantees', 'docs/case-studies/example.md');
expect(result).toBe(true);
});
test('should reject prohibited terms in normal content', () => {
const line = 'This guarantees results';
const result = scanner.isAllowedContext(line, 'guarantees', 'README.md');
expect(result).toBe(false);
});
});
describe('File Inclusion/Exclusion', () => {
test('should include markdown files', () => {
expect(scanner.includePatterns).toContain('**/*.md');
});
test('should include HTML files', () => {
expect(scanner.includePatterns).toContain('**/*.html');
});
test('should include JavaScript files', () => {
expect(scanner.includePatterns).toContain('**/*.js');
});
test('should exclude node_modules', () => {
expect(scanner.excludePatterns).toContain('**/node_modules/**');
});
test('should exclude .git directory', () => {
expect(scanner.excludePatterns).toContain('**/.git/**');
});
test('should exclude .claude directory', () => {
expect(scanner.excludePatterns).toContain('**/.claude/**');
});
});
describe('Edge Cases', () => {
test('should handle empty files', async () => {
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'empty.md');
await fs.writeFile(testFile, '');
const violations = await scanner.scan();
// Should not error on empty files
expect(Array.isArray(violations)).toBe(true);
});
test('should handle files with only whitespace', async () => {
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'whitespace.md');
await fs.writeFile(testFile, '\n\n \n\t\n');
const violations = await scanner.scan();
expect(Array.isArray(violations)).toBe(true);
});
test('should handle very long lines', async () => {
const longLine = 'a'.repeat(10000) + ' guarantee ' + 'b'.repeat(10000);
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'long.md');
await fs.writeFile(testFile, longLine);
const violations = await scanner.scan();
expect(violations.length).toBeGreaterThan(0);
});
test('should handle violations at file end without newline', async () => {
const testContent = 'This guarantees results';
await fs.mkdir(testFilesDir, { recursive: true });
const testFile = path.join(testFilesDir, 'test.md');
await fs.writeFile(testFile, testContent);
const violations = await scanner.scan();
expect(violations.length).toBeGreaterThan(0);
});
});
describe('Silent Mode', () => {
test('should suppress console output in silent mode', async () => {
const silentScanner = new ProhibitedTermsScanner({
silent: true,
basePath: testFilesDir
});
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
await silentScanner.scan();
// In silent mode, should not call console.log for scanning message
const scanningCalls = consoleSpy.mock.calls.filter(call =>
call.some(arg => typeof arg === 'string' && arg.includes('Scanning'))
);
expect(scanningCalls.length).toBe(0);
consoleSpy.mockRestore();
});
});
});