95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
#!/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()
|