#!/usr/bin/env node /** * Debt marker 검사 스크립트 (Issue #586) * * tech-debt-analyzer의 naive 부분 문자열 매칭(false positive) 대신 * actionable 마커만 보고한다. * * 사용법: * npx tsx scripts/check-debt-markers.ts [--production-only] [--path=] */ import { readFileSync, readdirSync, statSync } from 'fs'; import { join, relative } from 'path'; interface Finding { file: string; line: number; marker: string; message: string; code: string; } const ROOT = process.cwd(); const DEFAULT_SCAN_ROOT = join(ROOT, 'packages/memento-core/src'); const EXPLICIT_COMMENT_MARKERS = /\b(TODO|FIXME|HACK|XXX)\b/i; const EXPLICIT_BUG_COMMENT = /\bBUG\b/; const PRODUCTION_SKIP = /\.spec\.(ts|tsx)$|\/__tests__\//; /** 로그/진단 레벨 등 의도적 패턴 — actionable에서 제외 */ const IGNORE_LINE_PATTERNS: RegExp[] = [ /['"]debug['"]/, /\.debug\s*\(/, /logger\.debug/, /mcpLogger\.log(Server|Batch)\(\s*['"]debug['"]/, /LogLevel\.DEBUG/, /DEBUG\s*=\s*['"]DEBUG['"]/, /level === ['"]debug['"]/, /dataLevel === ['"]debug['"]/, /MCP_DEBUG|MEMENTO_DB_DEBUG|DEBUG_PERFORMANCE/, /getCurrentLogLevel|shouldLog|logWithMCPLogger\(['"]debug['"]/, /export type LogLevel = ['"]debug['"]/, /debug:\s*\d+/, /\/\*\*?\s*DEBUG/, /DEBUG 레벨/, /\/\/\s*.*진단\s*레벨/, /status IN \([^)]*'deprecated'[^)]*\)/, /\[deprecated\]/i, /deprecated 예정/, /deprecated\s+—/i, /테스트 호환용/, /리팩토링이 완료되어 더 이상 필요하지 않습니다/, /type-param-rollout\.md/, ]; function parseArgs(): { productionOnly: boolean; scanRoot: string } { let productionOnly = false; let scanRoot = DEFAULT_SCAN_ROOT; for (const arg of process.argv.slice(2)) { if (arg === '--production-only') productionOnly = true; else if (arg.startsWith('--path=')) scanRoot = arg.slice('--path='.length); else if (arg === '--help' || arg === '-h') { console.log('Usage: npx tsx scripts/check-debt-markers.ts [--production-only] [--path=]'); process.exit(0); } } return { productionOnly, scanRoot }; } function collectTsFiles(dir: string, productionOnly: boolean): string[] { const files: string[] = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); const st = statSync(full); if (st.isDirectory()) { files.push(...collectTsFiles(full, productionOnly)); continue; } if (!full.endsWith('.ts') && !full.endsWith('.tsx')) continue; const rel = relative(ROOT, full); if (productionOnly && PRODUCTION_SKIP.test(rel)) continue; files.push(full); } return files; } function isIgnoredLine(line: string): boolean { return IGNORE_LINE_PATTERNS.some((p) => p.test(line)); } function scanFile(filePath: string): Finding[] { const rel = relative(ROOT, filePath); const lines = readFileSync(filePath, 'utf8').split('\n'); const findings: Finding[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('*')) continue; if (isIgnoredLine(line)) continue; // @deprecated JSDoc: docs/architecture/core-deprecated-inventory.md 로 추적 (스캔 대상 아님) if (EXPLICIT_COMMENT_MARKERS.test(line) && /\/\/|\/\*/.test(line)) { const m = line.match(EXPLICIT_COMMENT_MARKERS); findings.push({ file: rel, line: i + 1, marker: (m?.[1] ?? 'MARKER').toUpperCase(), message: 'Explicit debt comment', code: trimmed.slice(0, 120), }); continue; } if (EXPLICIT_BUG_COMMENT.test(line) && /\/\/|\/\*/.test(line)) { findings.push({ file: rel, line: i + 1, marker: 'BUG', message: 'Explicit BUG comment', code: trimmed.slice(0, 120), }); continue; } // Substring bug in comments/strings (exclude log debug — handled by ignore list) if (/(?