Files
HolodoriCalc/scripts/summarize-wf-audit.mjs
T
dal4segno eb294ff5e3
Publish GitHub Pages output / publish (push) Successful in 47s
fix: repair SUS timing and note classification for 18 charts
2026-09-09 14:31:25 +09:00

48 lines
4.4 KiB
JavaScript

import { readFile, writeFile } from 'node:fs/promises';
import vm from 'node:vm';
const report = JSON.parse(await readFile('data/wf-audit/latest.json','utf8'));
const audit = await readFile('scripts/audit-wf-charts.mjs','utf8');
const context = vm.createContext({});
vm.runInContext(audit.slice(audit.indexOf('const counts ='),audit.indexOf('async function worker()')),context);
const simulator = await readFile('data/wf-audit/simulator.js','utf8');
const catalogText=simulator.match(/\b\w+=JSON\.parse\(`(\[\{"key":"m[\s\S]*?)`\)/)?.[1];
if(!catalogText)throw new Error('wf chart catalog not found');
const catalog = JSON.parse(catalogText.replaceAll('\\\\','\\').replaceAll('\\`','`').replaceAll('\\${','${'));
const collection=JSON.parse(await readFile('data/raw/sus/collection-manifest.json','utf8'));
for(const row of report.rows) {
const data=JSON.parse(await readFile(`data/wf-audit/charts/${row.songId}.${row.difficulty}.json`,'utf8'));
const local=JSON.parse(await readFile(`static/data/charts/${row.songId}.${row.difficulty}.json`,'utf8'));
const events=data.note_events;
// Match pn() in the saved wf simulator: preserve its explicit note_type/is_flick.
const native=[...(events.blue??[]).map(n=>({...n,type:'regular',isFlick:n.is_flick??false})),
...(events.pink??[]).map(n=>({...n,type:'regular',isFlick:n.is_flick??true})),
...(events.green_blocks??[]).flatMap(block=>block.map((n,i)=>({...n,type:n.note_type??(i===0||i===block.length-1?'regular':'mid'),isFlick:n.is_flick??false})))
].map(n=>({timeSeconds:n.time_seconds,type:n.type,isFlick:n.isFlick}));
context.left=local.notes;context.right=native;
row.localVsWfNative=vm.runInContext('compare(left,right)',context);
row.wfNativeCounts=vm.runInContext('counts(right)',context);
row.wfNativeCategoryMatchesSus=Object.keys(row.susExpected).every(k=>row.susExpected[k]===row.wfNativeCounts[k]);
row.wfSourceMatchesSus=row.wfSource.sha256===collection.files.find(f=>f.name===`chart_${row.songId}_${row.difficulty}.sus`).sha256;
const metadata=catalog.find(c=>c.key===`${row.songId}::${row.difficulty.toUpperCase()}`);
row.ratio={local:local.scoreRatio,wfCatalog:metadata?.scoreRatio,wfCatalogSource:metadata?.scoreCalibrationSource};
}
report.summary.wfNativeCategoryMismatchSus=report.rows.filter(r=>!r.wfNativeCategoryMatchesSus).length;
report.summary.localVsWfNativeMatches=report.rows.filter(r=>!r.localVsWfNative.extra&&!r.localVsWfNative.missing).length;
report.summary.wfSourceHashMismatchSus=report.rows.filter(r=>!r.wfSourceMatchesSus).length;
await writeFile('data/wf-audit/latest.json',JSON.stringify(report,null,2));
const lines=['# wf-calc 전체 채보 비교','',`비교 시각: ${report.generatedAt}`,`스냅샷: ${report.simulatorUrl}`,'',
'## 해석','',
'- wf 원본: 사이트의 pn() 함수처럼 note_type/is_flick을 그대로 읽은 데이터입니다. 사이트의 최종 점수 엔진 전체를 재현했다는 뜻은 아닙니다.',
'- 우리 어댑터: 기존 수집기의 flattenEvents를 거친 결과입니다. 이 단계의 분류 차이를 wf 자체 오류라고 부르면 안 됩니다.',
'- 시간 비교 허용치는 1마이크로초이며 동시 노트는 개수까지 비교합니다.',
'- wf 카탈로그 scoreRatio는 estimated 등 출처가 표시된 카탈로그 값입니다. 사이트에서 최종 계산 시 사용하는 값과 같은지는 별도 검증이 필요합니다.',
'- 원본 SUS 헤더는 노트 종류별 개수와 분모 검증에 사용했습니다. 기존 SUS 타이밍 재구성 후보의 미해결 사항은 그대로 남아 있습니다.','',
'## 집계','','```json',JSON.stringify(report.summary,null,2),'```','',
'## Mekurumeku Rendezvous Expert','','```json',JSON.stringify(report.rows.find(r=>r.songId==='m0527'&&r.difficulty==='expert'),null,2),'```','',
'## 전곡 결과','','|곡|난이도|우리 어댑터 적용 후 차이 초과/누락|wf 원본 분류 차이 초과/누락|스페셜 차이|wf 원본 해시 변경|', '|---|---|---|---|---|---|'];
for(const r of report.rows)lines.push(`|${r.title}|${r.difficulty}|${r.localVsWf.extra}/${r.localVsWf.missing}|${r.localVsWfNative.extra}/${r.localVsWfNative.missing}|${r.localSpecialMismatch}|${!r.wfSourceMatchesSus}|`);
await writeFile('data/wf-audit/latest.md',lines.join('\n')+'\n');
console.log(JSON.stringify(report.summary,null,2));
console.log(JSON.stringify(report.rows.find(r=>r.songId==='m0527'&&r.difficulty==='expert'),null,2));