#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import ts from 'typescript';
const ROOT = path.resolve(import.meta.dirname, '..');
const REF_DIR = path.join(ROOT, 'references');
const ALLOWED_PREFIXES = [
'packages/core',
'packages/shared',
'packages/mcp-servers',
'packages/web',
'apps/web',
'apps/electron',
'apps/cli',
'server/src',
];
const BASELINE = { 'as-any': 0, 'explicit-any': 0, 'console-log': 0 };
const fail = [];
const warn = [];
const bump = (m) => fail.push(m);
const note = (m) => warn.push(m);
function* mdBlocks(file) {
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const m = /^```(typescript|tsx|ts|javascript|js)\s*$/.exec(lines[i]);
if (!m) continue;
const fence = i + 1;
const body = [];
i++;
while (i < lines.length && !/^```\s*$/.test(lines[i])) body.push(lines[i++]);
yield { file, fence, lang: m[1], code: body.join('\n'), body };
}
}
function blockParses(code, lang) {
const kind = lang === 'tsx' || /<[A-Za-z][^>]*className=/.test(code) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const candidates = [
code,
`class __Snippet__ {\n${code}\n}`,
`namespace __Snippet__ {\n${code}\n}`,
`async function* __snippet__() {\n${code}\n}`,
];
for (const src of candidates) {
const sf = ts.createSourceFile('block', src, ts.ScriptTarget.Latest, true, kind);
if (!sf.parseDiagnostics || sf.parseDiagnostics.length === 0) return true;
}
return false;
}
const refFiles = fs.readdirSync(REF_DIR).filter((f) => f.endsWith('.md')).sort();
const allFiles = [path.join(ROOT, 'SKILL.md'), ...refFiles.map((f) => path.join(REF_DIR, f))];
let blocks = 0;
for (const f of allFiles) {
for (const b of mdBlocks(f)) {
blocks++;
if (blockParses(b.code, b.lang)) continue;
const kind = b.lang === 'tsx' || /<[A-Za-z][^>]*className=/.test(b.code) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
const sf = ts.createSourceFile('block', b.code, ts.ScriptTarget.Latest, true, kind);
const d = sf.parseDiagnostics || [];
const { line } = sf.getLineAndCharacterOfPosition(d[0].start);
const rel = path.relative(ROOT, f);
bump(`SYNTAX ${rel}:${b.fence + line + 1} (${d.length} errors) — ${b.body[line]?.trim().slice(0, 70)}`);
}
}
const stripFences = (t) => t.replace(/^```[\s\S]*?^```$/gm, '');
for (const f of allFiles.concat(['README.md', 'README.zh.md', 'README.en.md', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md', 'assets/QUICK_START.md', 'assets/PRE_LAUNCH_CHECKLIST.md'].map((p) => path.join(ROOT, p)))) {
if (!fs.existsSync(f)) continue;
const text = stripFences(fs.readFileSync(f, 'utf8'));
const rel = path.relative(ROOT, f);
for (const m of text.matchAll(/\]\((?!https?:|#|mailto:)([^)#\s]+)(#[^)]*)?\)/g)) {
const target = path.resolve(path.dirname(f), m[1]);
if (!fs.existsSync(target)) bump(`LINK ${rel} -> ${m[1]} does not exist`);
}
}
for (const f of refFiles.map((r) => path.join(REF_DIR, r))) {
const text = fs.readFileSync(f, 'utf8');
const rel = path.relative(ROOT, f);
for (const m of text.matchAll(/^\/\/ ((?:packages|apps|server|src)\/[A-Za-z0-9._/-]+)/gm)) {
if (!ALLOWED_PREFIXES.some((p) => m[1].startsWith(p))) bump(`LAYOUT ${rel} -> ${m[1]} is outside the declared monorepo layout`);
}
}
for (const r of refFiles) {
const text = fs.readFileSync(path.join(REF_DIR, r), 'utf8');
const pitfalls = (text.match(/🤖/g) || []).length;
if (pitfalls < 3) bump(`PROMISE references/${r} -> ${pitfalls} pitfall markers (SKILL.md requires >=3)`);
if (!/^##.*验收|^##.*[Cc]hecklist/m.test(text)) bump(`PROMISE references/${r} -> no verification-checklist section (SKILL.md claims every reference has one)`);
}
let asAny = 0, anyAnn = 0, clog = 0;
for (const r of refFiles) {
const t = fs.readFileSync(path.join(REF_DIR, r), 'utf8');
asAny += (t.match(/\bas any\b/g) || []).length;
anyAnn += (t.match(/: any\b/g) || []).length;
clog += (t.match(/console\.log\(/g) || []).length;
}
for (const [k, now] of [['as-any', asAny], ['explicit-any', anyAnn], ['console-log', clog]]) {
if (now > BASELINE[k]) bump(`REGRESS ${k}: ${now} > baseline ${BASELINE[k]}`);
else if (now < BASELINE[k]) note(`${k}: ${now} (baseline ${BASELINE[k]} — lower it)`);
}
console.log(`code blocks parsed: ${blocks}`);
for (const w of warn) console.log(' note ' + w);
for (const e of fail) console.log(' FAIL ' + e);
console.log(`\n${fail.length} failures, ${warn.length} notes`);
process.exit(fail.length ? 1 : 0);