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:
parent
6a80f344c1
commit
1fe50500f0
4 changed files with 973 additions and 3 deletions
440
scripts/framework-components/ProhibitedTermsScanner.js
Normal file
440
scripts/framework-components/ProhibitedTermsScanner.js
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
461
tests/unit/ProhibitedTermsScanner.test.js
Normal file
461
tests/unit/ProhibitedTermsScanner.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue