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,398 @@
#!/usr/bin/env node
'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const STATEFUL_API_PATTERN = /XMLHttpRequest|fetch|sendBeacon|readyState|readystatechange|loadend|abort|timeout|location\.reload|beforeunload|pagehide|visibilitychange|unload|Worker|SharedWorker|MessagePort|MessageChannel|postMessage|terminate|\.close|appendChild|removeChild|insertBefore|replaceChild|replaceChildren|innerHTML|textContent|querySelector|MutationObserver/i;
const MAX_TIMELINE_SEQUENCE = 10000;
function parseArgs(argv) {
const args = {
caseDir: '',
traces: [],
out: '',
baselineId: '',
json: false,
markdown: false,
help: false,
};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--case-dir' || arg === '--case' || arg === '-d') args.caseDir = argv[++i] || '';
else if (arg === '--trace' || arg === '--input' || arg === '-i') args.traces.push(argv[++i] || '');
else if (arg === '--out') args.out = argv[++i] || '';
else if (arg === '--baseline-id') args.baselineId = argv[++i] || '';
else if (arg === '--json') args.json = true;
else if (arg === '--markdown') args.markdown = true;
else if (arg === '--help' || arg === '-h') args.help = true;
else throw new Error(`未知参数:${arg}`);
}
if (!args.json && !args.markdown) args.markdown = true;
return args;
}
function usage() {
return `用法:
node scripts/build_trace_runtime_contract.js --case-dir case --markdown
node scripts/build_trace_runtime_contract.js --trace case/ruyi-trace/logs/trace.ndjson --out case/notes/trace-runtime-contract.json --baseline-id fp-001 --json
说明:从原始 Trace 生成逐 API、realm、receiver、访问类型和行为观测组成的 runtime 契约。该契约是后续 Node audit 的机器可验证输入,不是人工填写的覆盖清单。`;
}
function exists(file) {
try { return fs.existsSync(file); } catch { return false; }
}
function stat(file) {
try { return fs.statSync(file); } catch { return null; }
}
function walk(root, out = []) {
if (!exists(root)) return out;
const st = stat(root);
if (!st) return out;
if (st.isFile()) {
out.push(root);
return out;
}
if (!st.isDirectory()) return out;
for (const name of fs.readdirSync(root)) walk(path.join(root, name), out);
return out;
}
function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}
function stable(value) {
if (Array.isArray(value)) return value.map(stable);
if (!value || typeof value !== 'object') return value;
return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])]));
}
function digestValue(value) {
if (typeof value === 'undefined') return '';
try { return sha256(JSON.stringify(stable(value))); } catch { return sha256(String(value)); }
}
function rel(root, file) {
return (path.relative(root, file) || '.').replace(/\\/g, '/');
}
function discoverTraceFiles(args, caseDir) {
const files = [];
for (const item of args.traces) {
const file = path.resolve(item);
if (exists(file)) files.push(file);
}
if (!args.traces.length) {
const roots = [
path.join(caseDir, 'ruyi-trace', 'logs'),
path.join(caseDir, 'tmp'),
];
for (const root of roots) {
for (const file of walk(root)) {
const name = path.basename(file);
if (/\.(ndjson|jsonl|json)$/i.test(file) && /trace|ruyi|env/i.test(name) && !/audit|contract|summary/i.test(name)) {
files.push(file);
}
}
}
}
return [...new Set(files.map(file => path.resolve(file)))].sort();
}
function parseTraceFile(file) {
const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '');
if (/\.json$/i.test(file)) {
const raw = JSON.parse(text);
if (Array.isArray(raw)) return raw;
if (Array.isArray(raw.events)) return raw.events;
if (Array.isArray(raw.items)) return raw.items;
if (Array.isArray(raw.logs)) return raw.logs;
return [raw];
}
const events = [];
for (const [index, line] of text.split(/\r?\n/).entries()) {
if (!line.trim()) continue;
try {
const event = JSON.parse(line);
event.__line = index + 1;
events.push(event);
} catch (err) {
events.push({ type: 'parse-error', api: `line:${index + 1}`, error: { name: 'SyntaxError', message: err.message }, __line: index + 1 });
}
}
return events;
}
function firstValue(object, keys, fallback) {
if (arguments.length < 3) fallback = '';
for (const key of keys) {
if (object
&& Object.prototype.hasOwnProperty.call(object, key)
&& typeof object[key] !== 'undefined'
&& object[key] !== null
&& object[key] !== '') return object[key];
}
return fallback;
}
function normalizeText(value) {
if (typeof value === 'string') return value;
if (value === null || typeof value === 'undefined') return '';
if (typeof value === 'object') {
return firstValue(value, ['class', 'interface', 'name', 'type', 'id', 'path', 'value'], JSON.stringify(stable(value)));
}
return String(value);
}
function normalizeTraceMember(value) {
const text = normalizeText(value);
return text.replace(/^(get|set|call|construct)\s+/i, '').trim();
}
function normalizeStack(event) {
const stack = Array.isArray(event.stack) ? event.stack : [];
const frame = stack.find(item => item && (item.file || item.filename || item.url || item.line || item.lineNumber));
if (!frame) return '';
const file = firstValue(frame, ['file', 'filename', 'url'], '');
const line = firstValue(frame, ['line', 'lineNumber'], '');
const col = firstValue(frame, ['col', 'column', 'columnNumber'], '');
return [file, line, col].filter(value => value !== '').join(':');
}
function priorityOf(api, event) {
const explicit = normalizeText(firstValue(event, ['priority', 'level'], '')).toUpperCase();
if (/^P[0-2]$/.test(explicit)) return explicit;
if (/XMLHttpRequest|fetch|sendBeacon|window|document|location|navigator|cookie|Storage|Function\.prototype\.toString|reload|writer/i.test(api)) return 'P0';
if (/Object\.|Reflect\.|prototype|descriptor|EventTarget|Performance|Worker|Message|Canvas|WebGL|Audio|screen|Permissions|Plugin|MimeType|DOM|CSS/i.test(api)) return 'P1';
return 'P2';
}
function normalizeObservation(event) {
const interfaceName = normalizeText(firstValue(event, ['interface', 'interfaceName'], ''));
const member = normalizeTraceMember(firstValue(event, ['member', 'property', 'method'], ''));
const explicitApi = normalizeText(firstValue(event, ['api', 'path', 'name', 'prop'], ''));
const api = explicitApi || [interfaceName, member].filter(Boolean).join('.');
if (!api) return null;
const accessType = normalizeText(firstValue(event, ['accessType', 'operation', 'op', 'type'], 'unknown'));
const realm = normalizeText(firstValue(event, ['realmId', 'realm', 'contextId', 'frameId', 'workerId'], 'main'));
const receiver = normalizeText(firstValue(event, ['receiver', 'receiverType', 'thisType', 'target', 'object', 'thisObj'], ''));
const phase = normalizeText(firstValue(event, ['phase', 'writer', 'navigationPhase', 'stage'], ''));
const descriptor = firstValue(event, ['descriptor', 'propertyDescriptor'], undefined);
const prototypeChain = firstValue(event, ['prototypeChain', 'prototypes', 'protoChain'], undefined);
const ownKeys = firstValue(event, ['ownKeys', 'reflectOwnKeys'], undefined);
const ownPropertyNames = firstValue(event, ['ownPropertyNames', 'propertyNames'], undefined);
const ownSymbols = firstValue(event, ['ownSymbols', 'ownPropertySymbols'], undefined);
const result = firstValue(event, ['returnValue', 'result', 'value'], undefined);
const args = firstValue(event, ['arguments', 'args', 'parameters'], undefined);
const error = firstValue(event, ['error', 'exception', 'throw'], undefined);
const brand = normalizeText(firstValue(event, ['brand', 'toStringTag', 'objectToString'], ''));
const constructorName = normalizeText(firstValue(event, ['constructorName', 'constructor'], ''));
const owner = normalizeText(firstValue(event, ['owner', 'definedOn', 'prototypeOwner'], ''));
const sideEffects = firstValue(event, ['sideEffects', 'effects', 'mutations'], undefined);
return {
api,
accessType,
realm,
receiver,
phase,
owner,
brand,
constructorName,
descriptor,
prototypeChain,
ownKeys,
ownPropertyNames,
ownSymbols,
argsDigest: digestValue(args),
resultDigest: digestValue(result),
errorDigest: digestValue(error),
sideEffectsDigest: digestValue(sideEffects),
stack: normalizeStack(event),
sequence: Number(firstValue(event, ['sequence', 'seq', 'index', '__line'], 0)) || 0,
};
}
function contractKey(item) {
return [item.api, item.accessType, item.realm, item.receiver, item.phase].join('\u001f');
}
function addDistinct(target, value, limit = 20) {
if (value === '' || typeof value === 'undefined') return;
const digest = digestValue(value);
if (target.some(item => item.digest === digest)) return;
if (target.length < limit) target.push({ digest, value });
}
function orderStatefulTimeline(events) {
const sourceFiles = [...new Set(events.map(event => event.sourceFile))];
if (sourceFiles.length <= 1) {
return [...events].sort((a, b) => a.fileOrder - b.fileOrder);
}
const sequences = events.map(event => event.traceSequence);
const unique = new Set(sequences);
if (sequences.some(sequence => !Number.isFinite(sequence) || sequence <= 0)
|| unique.size !== sequences.length) {
throw new Error('多文件 Trace 无法建立唯一全局时间线;请合并为单一完整 Trace,或确保所有事件包含跨文件唯一递增 sequence。');
}
return [...events].sort((a, b) => a.traceSequence - b.traceSequence);
}
function buildStatefulTimeline(events) {
const ordered = orderStatefulTimeline(events);
const sequence = [];
for (const event of ordered) {
if (sequence[sequence.length - 1] !== event.contractId) sequence.push(event.contractId);
}
return {
schemaVersion: 'trace-runtime-timeline/v1',
required: sequence.length > 0,
eventCount: ordered.length,
collapsedCount: sequence.length,
truncated: sequence.length > MAX_TIMELINE_SEQUENCE,
sourceFiles: [...new Set(ordered.map(event => event.sourceFile))],
sequence: sequence.slice(0, MAX_TIMELINE_SEQUENCE),
sequenceSha256: sha256(JSON.stringify(sequence)),
};
}
function buildContract(args) {
const caseDir = path.resolve(args.caseDir || '.');
const files = discoverTraceFiles(args, caseDir);
if (!files.length) throw new Error('未找到原始 Trace 文件;请使用 --trace 指定,或把日志放入 case/ruyi-trace/logs/。');
if (!args.traces.length && files.length > 1) {
throw new Error(`自动发现 ${files.length} 份原始 Trace;禁止跨历史采集静默合并。请使用 --trace 显式选择当前 baseline 的完整 Trace,分片文件可重复传入 --trace。`);
}
const sourceFiles = [];
const groups = new Map();
const statefulTimelineEvents = [];
let eventCount = 0;
for (const file of files) {
const text = fs.readFileSync(file);
sourceFiles.push({ file: rel(caseDir, file), sha256: sha256(text), bytes: text.length });
const events = parseTraceFile(file);
for (const [fileEventOrder, event] of events.entries()) {
eventCount += 1;
const item = normalizeObservation(event);
if (!item) continue;
const key = contractKey(item);
if (!groups.has(key)) {
groups.set(key, {
id: sha256(key).slice(0, 24),
api: item.api,
accessType: item.accessType,
realm: item.realm,
receiver: item.receiver,
phase: item.phase,
priority: priorityOf(item.api, event),
count: 0,
sequences: [],
stacks: [],
assertions: {
owner: [],
brand: [],
constructorName: [],
descriptor: [],
prototypeChain: [],
ownKeys: [],
ownPropertyNames: [],
ownSymbols: [],
argsDigest: [],
resultDigest: [],
errorDigest: [],
sideEffectsDigest: [],
},
});
}
const group = groups.get(key);
group.count += 1;
if (item.sequence && group.sequences.length < 50) group.sequences.push(item.sequence);
if (item.stack && !group.stacks.includes(item.stack) && group.stacks.length < 10) group.stacks.push(item.stack);
for (const field of Object.keys(group.assertions)) addDistinct(group.assertions[field], item[field]);
if (STATEFUL_API_PATTERN.test(item.api)) {
statefulTimelineEvents.push({
contractId: group.id,
sourceFile: rel(caseDir, file),
fileOrder: fileEventOrder,
traceSequence: item.sequence,
realm: item.realm,
phase: item.phase,
api: item.api,
});
}
}
}
const contracts = [...groups.values()].sort((a, b) =>
a.priority.localeCompare(b.priority) || a.api.localeCompare(b.api) || a.realm.localeCompare(b.realm)
);
const traceSourceHash = sha256(JSON.stringify(sourceFiles.map(item => [item.file, item.sha256])));
const timeline = buildStatefulTimeline(statefulTimelineEvents);
const contract = {
schemaVersion: 'trace-runtime-contract/v3',
generatedBy: 'build_trace_runtime_contract.js',
generatedAt: new Date().toISOString(),
baselineId: args.baselineId || '',
traceSourceHash,
sourceFiles,
eventCount,
contractCount: contracts.length,
contracts,
timeline,
};
contract.contractHash = sha256(JSON.stringify(stable({
schemaVersion: contract.schemaVersion,
baselineId: contract.baselineId,
traceSourceHash: contract.traceSourceHash,
contracts: contract.contracts,
timeline: contract.timeline,
})));
const out = path.resolve(args.out || path.join(caseDir, 'notes', 'trace-runtime-contract.json'));
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(out, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
return { caseDir, out, contract };
}
function renderMarkdown(result) {
const p0 = result.contract.contracts.filter(item => item.priority === 'P0').length;
const p1 = result.contract.contracts.filter(item => item.priority === 'P1').length;
return [
'# Trace-runtime 行为契约生成结果',
'',
`- 输出:${result.out}`,
`- baselineId:${result.contract.baselineId || '未指定'}`,
`- Trace 文件数:${result.contract.sourceFiles.length}`,
`- Trace 事件数:${result.contract.eventCount}`,
`- 行为契约数:${result.contract.contractCount}`,
`- P0:${p0}`,
`- P1:${p1}`,
`- 状态事件:${result.contract.timeline.eventCount}`,
`- 折叠时间线:${result.contract.timeline.collapsedCount}`,
`- traceSourceHash:${result.contract.traceSourceHash}`,
`- contractHash:${result.contract.contractHash}`,
'',
'下一步必须运行 Node audit 并使用 check_trace_runtime_conformance.js 深度比较;不得手工把契约状态改成 matched。',
].join('\n') + '\n';
}
if (require.main === module) {
try {
const args = parseArgs(process.argv);
if (args.help) {
console.log(usage());
process.exit(0);
}
const result = buildContract(args);
if (args.json) console.log(JSON.stringify(result, null, 2));
if (args.markdown) process.stdout.write(renderMarkdown(result));
} catch (err) {
console.error(err.message || String(err));
console.error(usage());
process.exit(1);
}
}
module.exports = { buildContract, normalizeObservation, digestValue, stable };