v0.1.3: encoding remediation - restore 11 core files to clean UTF-8 + add check:encoding-health guard
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user