#!/usr/bin/env bun import fs from "node:fs" import path from "node:path" const ROOT = path.resolve(import.meta.dir, "..") const CORE_DIR = path.join(ROOT, "core") const SCAN_EXTENSIONS = new Set([".md", ".json", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".yaml", ".yml", ".txt"]) export const MOJIBAKE_SIGNATURES: { name: string; pattern: RegExp }[] = [ { name: "em-dash-mojibake", pattern: /\u9225/ }, { name: "right-arrow-mojibake", pattern: /\u922B/ }, { name: "check-mark-mojibake", pattern: /\u9257|\u9253/ }, { name: "cross-mark-mojibake", pattern: /\u9242/ }, { name: "geq-mojibake", pattern: /\u922E/ }, { name: "multiplication-mojibake", pattern: /\u8137/ }, { name: "replacement-char", pattern: /\uFFFD/ }, { name: "truncated-mojibake", pattern: /[\u9000-\u9FFF]\?/ }, { name: "gbk-cjk-bigram", pattern: /椤圭|鍒濆|閰嶇|鐨勶|鎷夎|浼氳瘽|鏃犲|淇濇|缁戝|瀹屾|涓珛|鏀瑰啓|瀹炰緥|鎵归|寤惰|璋冨|绀轰|鍘熸|閰嶉|鎹薄|閫昏|椤甸|绔犵|宸插|鍒嗛|鍏抽|鍚岀|鍚庣|鍓嶇|杩斿|杩涘|閫変|鎸夌|鏉冮|鏂囦|鏂囨/ }, ] export interface EncodingViolation { file: string line: number signature: string match: string } export function scanFile(filePath: string): EncodingViolation[] { const text = fs.readFileSync(filePath, "utf8") const violations: EncodingViolation[] = [] const lines = text.split(/\r?\n/) for (let i = 0; i < lines.length; i++) { for (const sig of MOJIBAKE_SIGNATURES) { const m = lines[i]!.match(sig.pattern) if (m) { violations.push({ file: filePath, line: i + 1, signature: sig.name, match: m[0]!, }) } } } return violations } export function collectFiles(dir: string): string[] { const out: string[] = [] if (!fs.existsSync(dir)) return out const walk = (d: string) => { for (const entry of fs.readdirSync(d, { withFileTypes: true })) { const full = path.join(d, entry.name) if (entry.isDirectory()) walk(full) else if (SCAN_EXTENSIONS.has(path.extname(entry.name))) out.push(full) } } walk(dir) return out.sort() } function main(): void { if (!fs.existsSync(CORE_DIR)) { console.error(`check-encoding-health: core/ directory not found at ${CORE_DIR} — cannot verify (fail-closed)`) process.exit(1) } const files = collectFiles(CORE_DIR) if (files.length === 0) { console.error(`check-encoding-health: 0 scannable files under ${CORE_DIR} — cannot verify (fail-closed)`) process.exit(1) } const violations: EncodingViolation[] = [] for (const f of files) { try { violations.push(...scanFile(f)) } catch (err) { console.error(`check-encoding-health: failed to read ${f}: ${(err as Error).message}`) process.exit(1) } } if (violations.length > 0) { console.error(`check-encoding-health: ${violations.length} mojibake violation(s) in core/:`) for (const v of violations) { const rel = path.relative(ROOT, v.file) console.error(` ${rel}:${v.line} [${v.signature}] ${JSON.stringify(v.match)}`) } process.exit(1) } console.log(`check-encoding-health: OK (${files.length} files scanned, 0 violations)`) } if (import.meta.main) main()