Files
STM32F4-Base/IEC60870-Slave/.build/command-v1/audit.cjs
T
STF cd42b4d61d
CI / L0 静态门禁 (pre-commit) (push) Failing after 2m51s
CI / L0.5 主机侧单测 (MSVC) (push) Successful in 18s
添加遥控业务代码
2026-10-30 20:03:19 -07:00

76 lines
3.8 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const root = path.resolve(__dirname, '../..');
const baseline = path.join(__dirname, 'before');
const manifest = JSON.parse(fs.readFileSync(path.join(baseline, 'manifest.json'), 'utf8').replace(/^\uFEFF/, ''));
const prior = new Map(manifest.map(x => [x.Path.replaceAll('\\', '/'), x]));
const files = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
if (entry.name.startsWith('.')) continue;
const file = path.join(dir, entry.name);
if (entry.isDirectory()) walk(file);
else if (entry.isFile()) files.push(file);
}
}
walk(root);
const errors = [], changed = [];
const utf8 = new TextDecoder('utf-8', {fatal: true});
for (const file of files) {
const relative = path.relative(root, file).replaceAll('\\', '/');
const bytes = fs.readFileSync(file);
const hash = crypto.createHash('sha256').update(bytes).digest('hex').toUpperCase();
if (prior.get(relative)?.Hash === hash) continue;
changed.push(relative);
if (!/\.(c|h|md|ps1|cjs)$/.test(file)) continue;
let text;
try { text = utf8.decode(bytes); } catch { errors.push(`${relative}: invalid UTF-8`); continue; }
const bom = bytes.length >= 3 && bytes.subarray(0, 3).equals(Buffer.from([239, 187, 191]));
if (prior.has(relative) && prior.get(relative).Bom !== bom) errors.push(`${relative}: BOM changed`);
if (text.includes('\uFFFD')) errors.push(`${relative}: replacement character`);
if (text.includes('\r')) errors.push(`${relative}: non-LF newline`);
if (!text.endsWith('\n')) errors.push(`${relative}: missing terminal newline`);
if (/[^\S\n]+\n/.test(text)) errors.push(`${relative}: trailing whitespace`);
}
let linkCount = 0, anchorCount = 0;
const anchorCache = new Map();
function anchors(file) {
if (anchorCache.has(file)) return anchorCache.get(file);
const text = fs.readFileSync(file, 'utf8').replace(/```[^\n]*\n[\s\S]*?```/g, '');
const found = new Set(), duplicates = new Map();
for (const match of text.matchAll(/\bid=["']([^"']+)["']/g)) found.add(match[1]);
for (const match of text.matchAll(/^#{1,6}\s+(.+)$/gm)) {
const slug = match[1].trim().toLowerCase().replace(/[^\p{L}\p{N}_\-\s]/gu, '').replace(/\s/g, '-');
const count = duplicates.get(slug) || 0;
found.add(count ? `${slug}-${count}` : slug);
duplicates.set(slug, count + 1);
}
anchorCache.set(file, found);
return found;
}
for (const file of files.filter(f => f.endsWith('.md'))) {
const text = fs.readFileSync(file, 'utf8').replace(/```[^\n]*\n[\s\S]*?```/g, '');
for (const match of text.matchAll(/\[[^\]\n]*\]\(([^)\n]+)\)/g)) {
let target = match[1].replace(/^<|>$/g, '').trim();
if (/^[a-z]+:/i.test(target)) continue;
const fragment = decodeURIComponent(target.split('#')[1] || '');
target = decodeURIComponent(target.split('#')[0]);
const resolved = target ? path.resolve(path.dirname(file), target) : file;
if (!resolved.toLowerCase().startsWith(root.toLowerCase() + path.sep)) {
errors.push(`${path.relative(root, file)}: link outside workspace ${target}`); continue;
}
++linkCount;
if (!fs.existsSync(resolved)) errors.push(`${path.relative(root, file)}: missing ${target}`);
else if (fragment && resolved.endsWith('.md')) {
++anchorCount;
if (!anchors(resolved).has(fragment)) errors.push(`${path.relative(root, file)}: missing anchor ${target}#${fragment}`);
}
}
}
for (const old of prior.keys()) {
if (!fs.existsSync(path.join(root, old))) errors.push(`Missing original file: ${old}`);
}
console.log(JSON.stringify({changed, checkedLocalLinks: linkCount, checkedAnchors: anchorCount, errors}, null, 2));
process.exitCode = errors.length ? 1 : 0;