#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); function main() { const outputsRoot = path.resolve(arg('--outputs') || arg('--output-root') || path.join(ROOT, 'outputs')); const outputDir = path.resolve(arg('--output') || path.join(outputsRoot, 'software-client-latest')); const input = arg('--input') || latestManualReviewSample(outputsRoot); const csv = path.join(outputDir, 'software-client-list.final.csv'); const markdown = path.join(outputDir, 'software-client-list.final.md'); const refreshSummary = path.join(outputDir, 'software-client-refresh-summary.json'); fs.mkdirSync(outputDir, { recursive: true }); if (!input) { const existingSummary = csv.replace(/\.csv$/i, '.summary.json'); const reusable = fs.existsSync(csv) && fs.existsSync(markdown) && fs.existsSync(existingSummary); const summary = { generatedAt: new Date().toISOString(), passed: reusable, skipped: true, reason: 'no_manual_review_sample_found', outputsRoot, outputDir, csv, markdown, summary: existingSummary }; fs.writeFileSync(refreshSummary, JSON.stringify(summary, null, 2), 'utf8'); console.log(JSON.stringify({ ...summary, refreshSummary }, null, 2)); if (!reusable) process.exitCode = 1; return; } runNode(['scripts/export-software-table.js', '--input', input, '--output', csv]); runNode(['scripts/export-software-markdown.js', '--input', csv, '--output', markdown]); const clientSummaryPath = csv.replace(/\.csv$/i, '.summary.json'); const clientSummary = readJson(clientSummaryPath); const summary = { generatedAt: new Date().toISOString(), passed: true, skipped: false, input, outputsRoot, outputDir, csv, markdown, summary: clientSummaryPath, sourceRows: Number(clientSummary.sourceRows || 0), outputRows: Number(clientSummary.outputRows || 0), sourceDuplicateRemovedCount: Number(clientSummary.sourceDuplicateRemovedCount || 0), finalDuplicateGroupCount: Number(clientSummary.finalDuplicateGroupCount || 0), duplicateCountMeaning: clientSummary.duplicateCountMeaning || '', rankContinuous: Boolean(clientSummary.rankContinuous) }; fs.writeFileSync(refreshSummary, JSON.stringify(summary, null, 2), 'utf8'); console.log(JSON.stringify({ ...summary, refreshSummary }, null, 2)); } function runNode(args) { const result = spawnSync(process.execPath, args, { cwd: ROOT, encoding: 'utf8', shell: false }); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); if (result.status !== 0) throw new Error(`${args[0]} failed with exit code ${result.status}`); } function latestManualReviewSample(root) { const files = listFiles(root).filter(file => path.basename(file) === 'manual-review-sample.csv'); const candidates = files .filter(isRefreshCandidateSample) .map(file => ({ file, sourceRows: manualReviewSampleRowCount(file), mtimeMs: fs.statSync(file).mtimeMs })) .filter(item => item.sourceRows > 0) .sort((a, b) => b.sourceRows - a.sourceRows || b.mtimeMs - a.mtimeMs); return candidates[0]?.file || ''; } function isRefreshCandidateSample(file) { const normalized = file.replace(/\\/g, '/').toLowerCase(); const parent = path.basename(path.dirname(file)); return /^overnight-quality-\d+$/.test(parent) && !normalized.includes('smoke'); } function manualReviewSampleRowCount(file) { const rows = parseCsv(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); return rows.slice(1).filter(row => row.some(cell => String(cell || '').trim())).length; } function listFiles(dir) { if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => { const full = path.join(dir, entry.name); return entry.isDirectory() ? listFiles(full) : [full]; }); } function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } function parseCsv(text) { const rows = []; let row = []; let cell = ''; let quoted = false; for (let index = 0; index < text.length; index += 1) { const char = text[index]; if (char === '\r') continue; if (char === '"' && quoted && text[index + 1] === '"') { cell += '"'; index += 1; } else if (char === '"') { quoted = !quoted; } else if (char === ',' && !quoted) { row.push(cell); cell = ''; } else if (char === '\n' && !quoted) { row.push(cell); rows.push(row); row = []; cell = ''; } else { cell += char; } } if (cell || row.length) { row.push(cell); rows.push(row); } return rows; } function arg(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : ''; } if (require.main === module) main();