import { mkdir, readFile, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { prepareHolodoriDbSource, holodoriGit as git } from './holodoridb-source.mjs'; import { jsonChanges } from './holodoridb-diff.mjs'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const output = path.join(root, 'data/holodoridb-review'); const baselineIndex = process.argv.indexOf('--baseline-ref'); const baselineRef = baselineIndex < 0 ? null : process.argv[baselineIndex + 1]; if (baselineIndex >= 0 && (!baselineRef || baselineRef.startsWith('-'))) throw new Error('--baseline-ref requires a git revision.'); const manifest = JSON.parse(baselineRef ? git(root, ['show', `${baselineRef}:data/holodoridb/manifest.json`]) : await readFile(path.join(root, 'data/holodoridb/manifest.json'), 'utf8')); const report = { generatedAt: new Date().toISOString(), baselineCollectedAt: manifest.generatedAt, sources: {} }; const lines = ['# HolodoriDB 전체 변경 검토', '', `마지막 수집: ${manifest.generatedAt}`, `검토 시각: ${report.generatedAt}`, '', '계산기 필요 여부로 필터링하지 않은 전체 원격 변경입니다. 검토는 수집 기준 커밋이나 계산기 데이터를 갱신하지 않습니다.', '']; const patches = []; for (const locale of ['eng', 'kor', 'jpn']) { const source = prepareHolodoriDbSource(locale, { review: true }); const from = manifest.sources[locale].commit; const to = source.commit; // Full history is necessary: endpoint diffs alone miss changes later reverted. if (from !== to) { const shallow = git(source.repositoryPath, ['rev-parse', '--is-shallow-repository']) === 'true'; git(source.repositoryPath, ['fetch', ...(shallow ? ['--unshallow'] : []), '--no-tags', source.repository, 'refs/heads/main']); try { git(source.repositoryPath, ['merge-base', '--is-ancestor', from, to]); } catch (error) { throw new Error(`${locale}: previous collection is not an ancestor of remote main; cannot certify a complete change report.`, { cause: error }); } } const commits = from === to ? [] : git(source.repositoryPath, ['rev-list', '--reverse', '--topo-order', `${from}..${to}`]).split('\n').filter(Boolean); const entry = { from, to, repository: source.repository, commitDate: source.commitDate, commits: [], netDiff: '' }; lines.push(`## ${locale}`, '', `기준: ${from}`, `최신: ${to}`, ''); if (!commits.length) lines.push('변경 없음.', ''); for (const commit of commits) { const title = git(source.repositoryPath, ['show', '-s', '--format=%s', commit]); const date = git(source.repositoryPath, ['show', '-s', '--format=%cI', commit]); const files = git(source.repositoryPath, ['diff-tree', '--root', '-m', '--no-commit-id', '--name-status', '-r', '--no-renames', commit]); const numstat = git(source.repositoryPath, ['show', '--format=', '--numstat', '--no-renames', '-m', commit]); const patch = git(source.repositoryPath, ['show', '--format=fuller', '--binary', '--no-ext-diff', '--no-renames', '-m', commit]); const parents = git(source.repositoryPath, ['show', '-s', '--format=%P', commit]).split(' ').filter(Boolean); const details = []; for (const parent of parents.length ? parents : [null]) { const changed = parent ? git(source.repositoryPath, ['diff', '--name-status', '--no-renames', parent, commit]) : files; for (const line of changed.split('\n').filter(Boolean)) { const [status, filename] = line.split('\t'); if (!filename.endsWith('.json')) continue; const before = status === 'A' ? undefined : JSON.parse(git(source.repositoryPath, ['show', `${parent}:${filename}`])); const after = status === 'D' ? undefined : JSON.parse(git(source.repositoryPath, ['show', `${commit}:${filename}`])); details.push({ parent, filename, changes: jsonChanges(before, after) }); } } entry.commits.push({ commit, title, date, files, numstat, details }); patches.push(`Repository: ${source.repository}\n${patch}\n`); const fileLines = files.split('\n').filter(Boolean); const strings = fileLines.filter(line => /\tLang[^\t]*\.json$/.test(line)); const masters = fileLines.filter(line => /\.json$/.test(line) && !strings.includes(line)); const other = fileLines.filter(line => !strings.includes(line) && !masters.includes(line)); lines.push(`### ${commit.slice(0, 8)} — ${title}`, '', `시각: ${date}`, '', `문자열 ${strings.length}건 · 마스터 JSON ${masters.length}건 · 기타 ${other.length}건`, ''); for (const [label, group] of [['문자열', strings], ['마스터 JSON', masters], ['기타', other]]) { if (group.length) lines.push(`**${label}** (A 추가 / M 수정 / D 삭제)`, '', '```text', ...group, '```', ''); } lines.push('파일별 추가·삭제 줄 수:', '', '```text', numstat, '```', ''); for (const detail of details) { const counts = Object.fromEntries(['added', 'deleted', 'modified'].map(type => [type, detail.changes.filter(change => change.type === type).length])); lines.push(`- ${detail.filename}: 항목/필드 추가 ${counts.added}, 삭제 ${counts.deleted}, 수정 ${counts.modified}`); } lines.push('', '전체 변경 항목의 ID·필드 경로·변경 전후 값은 latest.json의 details에 기록했습니다.', ''); } entry.netDiff = from === to ? '' : git(source.repositoryPath, ['diff', '--stat', from, to]); report.sources[locale] = entry; } lines.push('## 후속 작업', '', 'changes.patch의 전체 필드·문자열 변경을 읽고 사용자에게 의미와 영향을 요약해 먼저 제공합니다. 그 다음 계산기 데이터 확보를 진행합니다.', '', '계산기 반영: `npm run data:apply`. 원격 커밋이 검토 이후 바뀌면 다시 검토해야 합니다.', ''); await mkdir(output, { recursive: true }); await writeFile(path.join(output, 'changes.patch'), patches.join('\n'), 'utf8'); await writeFile(path.join(output, 'latest.md'), lines.join('\n'), 'utf8'); // Publish the machine-readable review marker only after all three locales succeed. await writeFile(path.join(output, 'latest.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8'); console.log(lines.join('\n')); console.log(`Full changes: ${path.join(output, 'changes.patch')}`);