reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,518 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
function parseArgs(argv) {
const args = { caseDir: '', fixture: '', envFile: '', require: '', json: false, markdown: false };
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i] || '';
else if (a === '--fixture') args.fixture = argv[++i] || '';
else if (a === '--env-file') args.envFile = argv[++i] || '';
else if (a === '--require') args.require = argv[++i] || '';
else if (a === '--json') args.json = true;
else if (a === '--markdown') args.markdown = true;
else if (a === '--help' || a === '-h') args.help = true;
else throw new Error(`未知参数:${a}`);
}
if (!args.json && !args.markdown) args.markdown = true;
return args;
}
function usage() {
return `用法:
node scripts/check_fingerprint_fixture.js --case-dir case --require canvas,webgl --markdown
node scripts/check_fingerprint_fixture.js --fixture case/fixtures/fingerprint.fixture.json --env-file case/result/src/env/fingerprint-env.js --json
说明:检查浏览器指纹 fixture 是否覆盖 Canvas / WebGL / WebGPU / Audio / DOM 几何等终端 API,是否绑定同一 fingerprint baseline,并检查最终 env 是否避免 node-canvas / headless-gl / 自动化浏览器等错误方向。`;
}
function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
function stat(p) { try { return fs.statSync(p); } catch { return null; } }
function readText(p) { return fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, ''); }
function readJson(p) { return JSON.parse(readText(p)); }
function rel(root, p) { return (path.relative(root, p) || '.').replace(/\\/g, '/'); }
function ext(p) { return path.extname(p).toLowerCase(); }
function walk(p, out = []) {
if (!exists(p)) return out;
const st = stat(p);
if (!st) return out;
if (st.isDirectory()) {
let names = [];
try { names = fs.readdirSync(p); } catch { names = []; }
for (const name of names) walk(path.join(p, name), out);
} else if (st.isFile()) out.push(p);
return out;
}
function countArray(v) { return Array.isArray(v) ? v.length : 0; }
function hasResultObject(v) { return !!v && typeof v === 'object' && Object.prototype.hasOwnProperty.call(v, 'result'); }
function pickBaselineId(obj) {
if (!obj || typeof obj !== 'object') return '';
return String(obj.baselineId || (obj.source && obj.source.baselineId) || '');
}
const BAD_VALUE_SOURCE_RE = /(AI\s*(?:推断|猜|生成|guess)|猜测|编造|伪造|默认值|随机值|mock|fake|placeholder|static\s*analysis|静态分析|inferred|guessed|random\s*value|jsdom|node-canvas|headless-gl)/i;
const REAL_VALUE_SOURCE_RE = /(RuyiTrace|NDJSON|ruyiPage|Camoufox|CloakBrowser|手动|真实浏览器|browser|Hook|HAR|MCP|用户提供|console|network)/i;
const TRACE_TOOL_RE = /(RuyiTrace|NDJSON)/i;
const AUTOMATION_OR_USER_TOOL_RE = /(ruyiPage|Camoufox|CloakBrowser|手动|真实浏览器|browser|Hook|HAR|MCP|用户提供|console|network)/i;
const LONG_VALUE_THRESHOLD = 3900;
const BAD_TRUNCATED_KEY_RE = /^(visiblePreview|visibleSha256|sha256OfVisible|visibleValue|visibleSnippet|preview|clippedPreview)$/i;
const BAD_TRUNCATED_TEXT_RE = /(\.\.\.<(?:clipped|truncated)>|<clipped>|<truncated>|__ruyiTraceLongString__|sha256OfVisible|visibleSha256|visiblePreview|realLength["']?\s*[:=]\s*["']?unknown)/i;
const CHUNK_ENCODING_RE = /^(string-chunks|base64-chunks)$/;
function jsonText(v) {
try { return JSON.stringify(v || {}); } catch { return String(v || ''); }
}
function sourceLabel(source) {
if (!source || typeof source !== 'object') return '';
return [source.capturedBy, source.tool, source.mode, source.sourceType, source.type, source.origin, source.evidence]
.filter(Boolean).map(String).join(' ');
}
function traceStatusOf(source, record) {
return String((record && record.traceStatus) || (source && source.traceStatus) || '');
}
function hasSourceMeta(source) {
if (!source || typeof source !== 'object') return false;
return !!(source.capturedBy || source.tool || source.mode || source.sourceType || source.type || source.origin || source.evidence);
}
function isTruncatedTraceStatus(status) {
const s = String(status || '');
if (/used-untruncated|untruncated|not-truncated/i.test(s)) return false;
return /(^|[-_\s])truncated($|[-_\s])|疑似截断|截断/i.test(s);
}
function isTraceValueSource(source, record) {
const status = traceStatusOf(source, record);
const label = sourceLabel(source);
if (/used-untruncated/i.test(status)) return true;
if (/truncated|missing|not-covered|baseline-conflict|unused/i.test(status) && AUTOMATION_OR_USER_TOOL_RE.test(label) && !TRACE_TOOL_RE.test(String(source && source.capturedBy || ''))) return false;
return TRACE_TOOL_RE.test(label);
}
function collectStringLengths(value, out = []) {
if (typeof value === 'string') out.push(value.length);
else if (Array.isArray(value)) for (const item of value) collectStringLengths(item, out);
else if (value && typeof value === 'object') for (const item of Object.values(value)) collectStringLengths(item, out);
return out;
}
function isChunkedValue(value) {
return !!(
value &&
typeof value === 'object' &&
CHUNK_ENCODING_RE.test(String(value.encoding || '')) &&
Array.isArray(value.chunks)
);
}
function hasOwn(obj, key) {
return !!(obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, key));
}
function hasLengthAndHashOn(value, record, source) {
if (isChunkedValue(value)) {
return Number.isFinite(value.valueLength) && typeof value.sha256 === 'string' && /^[a-f0-9]{64}$/i.test(value.sha256);
}
if (value && typeof value === 'object') {
const metrics = collectValueMetrics(value);
if (metrics.chunked.length) {
return metrics.chunked.every(info =>
Number.isFinite(info.value.valueLength) &&
typeof info.value.sha256 === 'string' &&
/^[a-f0-9]{64}$/i.test(info.value.sha256)
);
}
}
return (
(hasOwn(record, 'valueLength') || hasOwn(source, 'valueLength')) &&
(hasOwn(record, 'sha256') || hasOwn(source, 'sha256'))
);
}
function collectValueMetrics(value, out = { maxLen: 0, chunked: [], badMarkers: [] }, pathParts = []) {
const label = pathParts.join('.') || 'result';
if (typeof value === 'string') {
out.maxLen = Math.max(out.maxLen, value.length);
if (BAD_TRUNCATED_TEXT_RE.test(value)) out.badMarkers.push(`${label} 包含截断标记或可见片段标记`);
} else if (Array.isArray(value)) {
value.forEach((item, idx) => collectValueMetrics(item, out, pathParts.concat(String(idx))));
} else if (value && typeof value === 'object') {
if (isChunkedValue(value)) {
const joined = value.chunks.join('');
out.maxLen = Math.max(out.maxLen, Number(value.valueLength) || joined.length);
out.chunked.push({ path: label, value, joinedLength: joined.length });
}
for (const [key, item] of Object.entries(value)) {
if (BAD_TRUNCATED_KEY_RE.test(key)) out.badMarkers.push(`${label}.${key} 使用了 Trace 可见片段字段,不能作为最终值`);
collectValueMetrics(item, out, pathParts.concat(key));
}
}
return out;
}
function validateChunkedValue(itemPath, chunk) {
const problems = [];
const warnings = [];
const encoding = String(chunk.encoding || '');
const joined = Array.isArray(chunk.chunks) ? chunk.chunks.join('') : '';
if (!CHUNK_ENCODING_RE.test(encoding)) problems.push(`样本 ${itemPath} 的分片 encoding 不合法:${encoding || '未指定'}。`);
if (!Array.isArray(chunk.chunks) || chunk.chunks.some(v => typeof v !== 'string')) problems.push(`样本 ${itemPath} 的 chunks 必须是字符串数组。`);
if (!Number.isFinite(chunk.valueLength)) problems.push(`样本 ${itemPath} 的分片结果缺少数值型 valueLength。`);
if (!Number.isFinite(chunk.chunkSize) || chunk.chunkSize <= 0) problems.push(`样本 ${itemPath} 的分片结果缺少有效 chunkSize。`);
if (chunk.truncated !== false) problems.push(`样本 ${itemPath} 的分片结果必须显式 truncated:false。`);
if (Number.isFinite(chunk.valueLength) && joined.length !== chunk.valueLength) {
problems.push(`样本 ${itemPath} 的 chunks 拼接长度 ${joined.length} 与 valueLength ${chunk.valueLength} 不一致。`);
}
if (Number.isFinite(chunk.chunkSize)) {
const tooLarge = (chunk.chunks || []).find(v => v.length > chunk.chunkSize);
if (tooLarge) warnings.push(`样本 ${itemPath} 存在超过 chunkSize 的分片;请确认分片写入逻辑没有异常。`);
}
if (typeof chunk.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(chunk.sha256)) {
problems.push(`样本 ${itemPath} 的长指纹分片缺少完整 sha256;必须先在真实浏览器采样 Hook 中 finalize,再写入 fixture。`);
}
if (BAD_TRUNCATED_TEXT_RE.test(joined)) problems.push(`样本 ${itemPath} 的分片拼接结果包含截断标记,不能作为最终值。`);
return { problems, warnings };
}
function collectResultRecords(node, out = [], pathParts = []) {
if (!node || typeof node !== 'object') return out;
if (Object.prototype.hasOwnProperty.call(node, 'result')) {
out.push({ path: pathParts.join('.') || '$', record: node, value: node.result });
}
for (const [key, value] of Object.entries(node)) {
if (key === 'source' || key === 'valueSource' || key === 'match') continue;
if (value && typeof value === 'object') collectResultRecords(value, out, pathParts.concat(key));
}
return out;
}
function validateValueSources(fp) {
const problems = [];
const warnings = [];
const sourceSummary = {
totalRecords: 0,
inheritedSource: 0,
explicitSource: 0,
traceSource: 0,
sampledSource: 0,
longValueRecords: 0,
chunkedLongValueRecords: 0,
blockedTruncatedResultRecords: 0,
badSourceRecords: 0
};
const rootSource = fp && fp.source;
function validateSource(source, label, record, inherited) {
if (!source || typeof source !== 'object') {
problems.push(`${label} 缺少真实值来源 source / capturedBy;必须优先使用未截断 Trace,Trace 不可用时用已确认取证工具采样,不能由 AI 猜值。`);
return;
}
const sourceText = jsonText(source);
const labelText = sourceLabel(source);
const isFixtureRoot = label === 'fixture.source';
if (!hasSourceMeta(source)) problems.push(`${label} 的 source 缺少 mode / capturedBy / tool / sourceType 等来源字段。`);
if (!REAL_VALUE_SOURCE_RE.test(sourceText)) problems.push(`${label} 的 source 未体现真实浏览器证据来源;请记录 RuyiTrace 未截断值,或 ruyiPage / Camoufox / CloakBrowser / 手动浏览器采样来源。`);
if (BAD_VALUE_SOURCE_RE.test(sourceText)) {
if (!isFixtureRoot) sourceSummary.badSourceRecords += 1;
problems.push(`${label} 的 source 疑似为 AI 猜值、静态分析、默认值、随机值、mock 值或 Node.js 模拟库结果,不能作为最终指纹回放值。`);
}
const status = traceStatusOf(source, record);
if (!status) warnings.push(`${label} 缺少 traceStatus;建议记录 used-untruncated / unused / missing / not-covered / truncated / baseline-conflict,明确是否优先检查 Trace。`);
if (!isFixtureRoot && isTraceValueSource(source, record)) sourceSummary.traceSource += 1;
if (!isFixtureRoot && AUTOMATION_OR_USER_TOOL_RE.test(labelText)) sourceSummary.sampledSource += 1;
}
validateSource(rootSource, 'fixture.source', null, false);
const records = collectResultRecords(fp);
sourceSummary.totalRecords = records.length;
for (const item of records) {
const source = item.record.source || item.record.valueSource || rootSource;
const inherited = !(item.record.source || item.record.valueSource);
if (inherited) sourceSummary.inheritedSource += 1;
else sourceSummary.explicitSource += 1;
validateSource(source, `样本 ${item.path}`, item.record, inherited);
const metrics = collectValueMetrics(item.value);
const maxLen = metrics.maxLen;
if (maxLen >= LONG_VALUE_THRESHOLD) sourceSummary.longValueRecords += 1;
if (metrics.chunked.length) sourceSummary.chunkedLongValueRecords += metrics.chunked.length;
for (const marker of metrics.badMarkers) {
sourceSummary.blockedTruncatedResultRecords += 1;
problems.push(`样本 ${item.path} ${marker};不能把 RuyiTrace 可见片段、clip 片段或未知真实长度写入最终 fixture。`);
}
for (const chunkInfo of metrics.chunked) {
const chunkResult = validateChunkedValue(`${item.path}.${chunkInfo.path}`, chunkInfo.value);
problems.push(...chunkResult.problems);
warnings.push(...chunkResult.warnings);
}
const valueIsMarkedTruncated = item.record.truncated === true || (source && source.truncated === true);
const status = traceStatusOf(source, item.record);
if (valueIsMarkedTruncated) problems.push(`样本 ${item.path} 标记为 truncated=true,不能作为最终回放值;请用同一 baseline 下的取证工具补采完整值。`);
if (isTraceValueSource(source, item.record) && (maxLen >= LONG_VALUE_THRESHOLD || isTruncatedTraceStatus(status) || valueIsMarkedTruncated)) {
sourceSummary.blockedTruncatedResultRecords += 1;
problems.push(`样本 ${item.path} 来自 Trace 且疑似长字段截断;不能把 RuyiTrace 4000 / 4096 可见片段作为完整指纹值,请用同一 baseline 下的 ruyiPage / Camoufox / CloakBrowser / 手动浏览器补采完整值。`);
}
if (maxLen >= LONG_VALUE_THRESHOLD && inherited) {
problems.push(`样本 ${item.path} 是长字符串 / 大结果但未记录每条样本自己的 source、valueLength 和 sha256;请补充完整浏览器采样来源,避免误把 Trace 4000 / 4096 字符可见片段当成真实值。`);
}
if (maxLen >= LONG_VALUE_THRESHOLD && !hasLengthAndHashOn(item.value, item.record, source)) {
problems.push(`样本 ${item.path} 长度达到 ${maxLen},但缺少完整 valueLength 与 sha256;长指纹必须通过真实浏览器完整采样或分片保存并记录完整 hash。`);
}
}
return { problems, warnings, sourceSummary };
}
function inspectFixture(file) {
const problems = [];
const warnings = [];
const counts = {
canvasToDataURL: 0,
canvasToBlob: 0,
canvasMeasureText: 0,
canvasGetImageData: 0,
webglGetParameter: 0,
webglGetSupportedExtensions: 0,
webglGetExtension: 0,
webglGetShaderPrecisionFormat: 0,
webglReadPixels: 0,
webgpuRequestAdapter: 0,
audioStartRendering: 0,
audioGetChannelData: 0,
domGetBoundingClientRect: 0,
domOffset: 0,
};
let fixture = null;
if (!file || !exists(file)) {
problems.push(`未找到指纹 fixture:${file || '未指定'}`);
return { fixture, counts, problems, warnings };
}
try { fixture = readJson(file); } catch (err) {
problems.push(`指纹 fixture JSON 解析失败:${err.message}`);
return { fixture, counts, problems, warnings };
}
const fp = fixture.fingerprint && typeof fixture.fingerprint === 'object' ? fixture.fingerprint : fixture;
counts.canvasToDataURL = countArray(fp.canvas && fp.canvas.toDataURL);
counts.canvasToBlob = countArray(fp.canvas && fp.canvas.toBlob);
counts.canvasMeasureText = countArray(fp.canvas && fp.canvas.measureText);
counts.canvasGetImageData = countArray(fp.canvas && fp.canvas.getImageData);
counts.webglGetParameter = countArray(fp.webgl && fp.webgl.getParameter);
counts.webglGetSupportedExtensions = hasResultObject(fp.webgl && fp.webgl.getSupportedExtensions) ? 1 : countArray(fp.webgl && fp.webgl.getSupportedExtensions);
counts.webglGetExtension = countArray(fp.webgl && fp.webgl.getExtension);
counts.webglGetShaderPrecisionFormat = countArray(fp.webgl && fp.webgl.getShaderPrecisionFormat);
counts.webglReadPixels = countArray(fp.webgl && fp.webgl.readPixels);
counts.webgpuRequestAdapter = countArray(fp.webgpu && fp.webgpu.requestAdapter);
counts.audioStartRendering = countArray(fp.audio && fp.audio.startRendering);
counts.audioGetChannelData = countArray(fp.audio && fp.audio.getChannelData);
counts.domGetBoundingClientRect = countArray(fp.domGeometry && fp.domGeometry.getBoundingClientRect);
counts.domOffset = countArray(fp.domGeometry && fp.domGeometry.offset);
const baselineId = pickBaselineId(fp);
if (!baselineId) problems.push('指纹 fixture 缺少 baselineId;必须先创建 case/notes/fingerprint-baseline.json,并让 fixture 绑定同一 baselineId。');
if (!fp.source) problems.push('指纹 fixture 缺少 source 字段;必须记录真实值来源,优先使用未截断 Trace,Trace 不可用时使用已确认取证工具采样。');
const sourceResult = validateValueSources(fp);
problems.push(...sourceResult.problems);
warnings.push(...sourceResult.warnings);
return { fixture: fp, baselineId, counts, problems, warnings, sourceSummary: sourceResult.sourceSummary };
}
function inspectBaseline(caseDir, fixtureResult) {
const problems = [];
const warnings = [];
const baselineFile = path.join(caseDir, 'notes', 'fingerprint-baseline.json');
const result = { file: baselineFile, present: exists(baselineFile), baselineId: '', conflicts: [] };
if (!result.present) {
problems.push('缺少指纹基线文件 case/notes/fingerprint-baseline.json;涉及指纹采样时必须先固定同一 case 的 fingerprint baseline。');
return { result, problems, warnings };
}
let baseline;
try { baseline = readJson(baselineFile); } catch (err) {
problems.push(`指纹基线 JSON 解析失败:${err.message}`);
return { result, problems, warnings };
}
result.baselineId = String(baseline.baselineId || '');
if (!result.baselineId) problems.push('fingerprint-baseline.json 缺少 baselineId。');
if (fixtureResult.baselineId && result.baselineId && fixtureResult.baselineId !== result.baselineId) {
problems.push(`指纹 fixture baselineId 与基线不一致:fixture=${fixtureResult.baselineId},baseline=${result.baselineId}。不得混用不同随机指纹样本。`);
}
const fp = fixtureResult.fixture || {};
const source = fp.source || {};
const checks = [
['userAgent', source.userAgent, baseline.navigator && baseline.navigator.userAgent],
['timezone', source.timezone, baseline.network && baseline.network.timezone],
['locale', source.locale, baseline.network && baseline.network.locale],
];
for (const [name, a, b] of checks) {
if (a && b && String(a) !== String(b)) result.conflicts.push({ field: name, fixture: String(a), baseline: String(b) });
}
if (result.conflicts.length) {
problems.push(`指纹 fixture 与 baseline 核心字段冲突:${result.conflicts.map(x => `${x.field}: fixture=${x.fixture}, baseline=${x.baseline}`).join(';')}。请重新采样或生成新 baseline。`);
}
return { result, problems, warnings };
}
const REQUIRE_RULES = {
canvas: ['canvasToDataURL', 'canvasMeasureText', 'canvasGetImageData'],
webgl: ['webglGetParameter', 'webglGetSupportedExtensions', 'webglGetShaderPrecisionFormat', 'webglReadPixels'],
webgpu: ['webgpuRequestAdapter'],
audio: ['audioStartRendering', 'audioGetChannelData'],
'dom-geometry': ['domGetBoundingClientRect', 'domOffset'],
dom: ['domGetBoundingClientRect', 'domOffset'],
font: ['canvasMeasureText', 'domOffset'],
fonts: ['canvasMeasureText', 'domOffset'],
};
const BAD_RENDER_PATTERNS = [
{ name: 'node-canvas/canvas', pattern: /\b(require|import)\s*\(?\s*['"](?:canvas|node-canvas)['"]|from\s+['"](?:canvas|node-canvas)['"]/i },
{ name: 'headless-gl/gl', pattern: /\b(require|import)\s*\(?\s*['"](?:gl|headless-gl)['"]|from\s+['"](?:gl|headless-gl)['"]/i },
{ name: '浏览器自动化', pattern: /\b(playwright|puppeteer|selenium|cloakbrowser|ruyipage|page\.goto|browser\.launch|chromium\.launch)\b/i },
];
function inspectEnvCode(files, root) {
const problems = [];
const warnings = [];
const hits = [];
for (const file of files) {
if (!exists(file)) continue;
const text = readText(file);
for (const item of BAD_RENDER_PATTERNS) {
if (item.pattern.test(text)) hits.push({ file: rel(root, file), type: item.name });
}
if (/toDataURL|getImageData|measureText|getParameter|getBoundingClientRect/.test(text) && !/fingerprint|回放|replay|fixture/i.test(text)) {
warnings.push(`${rel(root, file)} 涉及指纹终端 API,但未明显体现 fixture / replay;请确认不是在 Node.js 中伪造渲染过程。`);
}
}
if (hits.length) {
problems.push(`发现不推荐的指纹实现方向:${hits.map(h => `${h.file}(${h.type})`).join('、')}。应使用真实浏览器采样值回放,不要在最终项目中依赖渲染库或自动化浏览器计算指纹。`);
}
return { problems, warnings, hits };
}
function defaultFixture(caseDir) {
const candidates = [
path.join(caseDir, 'fixtures', 'fingerprint.fixture.json'),
path.join(caseDir, 'fixtures', 'sample.fixture.json'),
];
return candidates.find(exists) || candidates[0];
}
function defaultEnvFiles(caseDir) {
const result = path.join(caseDir, 'result');
return walk(result).filter(p => ['.js', '.mjs', '.cjs'].includes(ext(p)));
}
function check(args) {
const caseDir = args.caseDir ? path.resolve(args.caseDir) : (args.fixture ? path.resolve(path.dirname(args.fixture), '..') : process.cwd());
const fixtureFile = args.fixture ? path.resolve(args.fixture) : defaultFixture(caseDir);
const envFiles = args.envFile ? [path.resolve(args.envFile)] : defaultEnvFiles(caseDir);
const required = String(args.require || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
const problems = [];
const warnings = [];
const fixtureResult = inspectFixture(fixtureFile);
problems.push(...fixtureResult.problems);
warnings.push(...fixtureResult.warnings);
for (const item of required) {
const rules = REQUIRE_RULES[item] || [];
if (!rules.length) warnings.push(`未知 require 类型:${item}`);
const ok = rules.some(k => fixtureResult.counts[k] > 0);
if (!ok) problems.push(`要求 ${item} 指纹样本,但 fixture 中未发现对应终端 API 返回值。`);
}
const baselineResult = inspectBaseline(caseDir, fixtureResult);
problems.push(...baselineResult.problems);
warnings.push(...baselineResult.warnings);
const envResult = inspectEnvCode(envFiles, caseDir);
problems.push(...envResult.problems);
warnings.push(...envResult.warnings);
return {
caseDir,
fixtureFile,
envFiles: envFiles.map(p => rel(caseDir, p)),
required,
clean: problems.length === 0,
baseline: baselineResult.result,
fixtureBaselineId: fixtureResult.baselineId || '',
counts: fixtureResult.counts,
sourceSummary: fixtureResult.sourceSummary || {
totalRecords: 0,
inheritedSource: 0,
explicitSource: 0,
traceSource: 0,
sampledSource: 0,
longValueRecords: 0,
chunkedLongValueRecords: 0,
blockedTruncatedResultRecords: 0,
badSourceRecords: 0
},
badImplementationHits: envResult.hits,
problems,
warnings,
};
}
function renderMarkdown(result) {
const lines = [
'# 指纹 fixture 与回放实现检查',
'',
`case 目录:${result.caseDir}`,
`fixture:${result.fixtureFile}`,
`是否通过:${result.clean ? '是' : '否'}`,
'',
'## 指纹基线',
`- baseline 文件:${result.baseline.file}`,
`- baseline 是否存在:${result.baseline.present ? '是' : '否'}`,
`- baselineId:${result.baseline.baselineId || '未发现'}`,
`- fixture baselineId:${result.fixtureBaselineId || '未发现'}`,
`- 核心字段冲突:${result.baseline.conflicts.length ? '是' : '否'}`,
'',
'## 样本覆盖统计',
];
for (const [k, v] of Object.entries(result.counts)) lines.push(`- ${k}:${v}`);
lines.push('', '## 指纹值来源检查');
lines.push(`- result 样本数量:${result.sourceSummary.totalRecords}`);
lines.push(`- 显式 source 样本:${result.sourceSummary.explicitSource}`);
lines.push(`- 继承全局 source 样本:${result.sourceSummary.inheritedSource}`);
lines.push(`- Trace 来源样本:${result.sourceSummary.traceSource}`);
lines.push(`- 自动化 / 手动浏览器采样来源样本:${result.sourceSummary.sampledSource}`);
lines.push(`- 长字段样本:${result.sourceSummary.longValueRecords}`);
lines.push(`- 分片长字段样本:${result.sourceSummary.chunkedLongValueRecords}`);
lines.push(`- 被阻断的截断 / 可见片段样本:${result.sourceSummary.blockedTruncatedResultRecords}`);
lines.push(`- 疑似猜值 / 默认值 / 模拟库来源样本:${result.sourceSummary.badSourceRecords}`);
lines.push('', '## 检查的 env 文件');
if (result.envFiles.length) for (const f of result.envFiles) lines.push(`- ${f}`);
else lines.push('- 未发现 result 下的 JS env 文件');
if (result.problems.length) {
lines.push('', '## 问题');
for (const p of result.problems) lines.push(`- ${p}`);
}
if (result.warnings.length) {
lines.push('', '## 提醒');
for (const w of result.warnings) lines.push(`- ${w}`);
}
return lines.join('\n') + '\n';
}
try {
const args = parseArgs(process.argv);
if (args.help) { console.log(usage()); process.exit(0); }
const result = check(args);
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(renderMarkdown(result));
process.exit(result.clean ? 0 : 1);
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}