mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-27 07:52:03 +00:00
249 lines
7.7 KiB
JavaScript
249 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
const VALID_CADENCES = ['daily', 'weekly'];
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
sinceDays: 14,
|
|
logRoot: path.resolve(process.cwd(), 'task-logs'),
|
|
outDir: path.resolve(process.cwd(), 'artifacts', 'lock-reliability'),
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const token = argv[i];
|
|
if (token === '--since-days') {
|
|
options.sinceDays = Number.parseInt(argv[i + 1] || '14', 10);
|
|
i += 1;
|
|
} else if (token === '--log-root') {
|
|
options.logRoot = path.resolve(process.cwd(), argv[i + 1] || 'task-logs');
|
|
i += 1;
|
|
} else if (token === '--out-dir') {
|
|
options.outDir = path.resolve(process.cwd(), argv[i + 1] || path.join('artifacts', 'lock-reliability'));
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
if (!Number.isFinite(options.sinceDays) || options.sinceDays < 0) {
|
|
throw new Error(`Invalid --since-days value: ${options.sinceDays}`);
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function parseFrontmatter(markdown) {
|
|
const text = String(markdown || '');
|
|
if (!text.startsWith('---\n')) return {};
|
|
const end = text.indexOf('\n---', 4);
|
|
if (end === -1) return {};
|
|
const frontmatter = text.slice(4, end).split(/\r?\n/);
|
|
const result = {};
|
|
for (const line of frontmatter) {
|
|
const match = line.match(/^([a-zA-Z0-9_]+):\s*(.+)\s*$/);
|
|
if (!match) continue;
|
|
const [, key, rawValue] = match;
|
|
result[key] = rawValue.replace(/^'/, '').replace(/'$/, '').replace(/''/g, "'");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function inc(map, key) {
|
|
map.set(key, (map.get(key) || 0) + 1);
|
|
}
|
|
|
|
function collectRelayEndpoints({ frontmatter, body }) {
|
|
const relays = new Set();
|
|
const relayList = frontmatter.relay_list || '';
|
|
relayList.split(',').map((v) => v.trim()).filter(Boolean).forEach((relay) => relays.add(relay));
|
|
|
|
const allText = `${body}\n${Object.values(frontmatter).join('\n')}`;
|
|
const urlMatches = allText.match(/wss?:\/\/[^\s'`),]+/g) || [];
|
|
for (const url of urlMatches) {
|
|
relays.add(url.trim());
|
|
}
|
|
|
|
if (!relays.size) relays.add('unknown');
|
|
return [...relays];
|
|
}
|
|
|
|
function getBackendCategory({ frontmatter, body }) {
|
|
if (frontmatter.backend_category) return frontmatter.backend_category;
|
|
if (frontmatter.preflight_failure_category) return `preflight:${frontmatter.preflight_failure_category}`;
|
|
|
|
const reasonMatch = body.match(/- reason:\s*([^\n]+)/i) || body.match(/reason:\s*([^\n]+)/i);
|
|
const reason = reasonMatch?.[1]?.trim() || '';
|
|
|
|
if (/lock backend/i.test(reason)) return 'lock backend error (uncategorized)';
|
|
if (/completion publish failed/i.test(reason)) return 'completion publish failed';
|
|
if (/preflight/i.test(reason)) return 'preflight failed (uncategorized)';
|
|
return null;
|
|
}
|
|
|
|
function toSortedObject(counterMap) {
|
|
return Object.fromEntries([...counterMap.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
|
|
}
|
|
|
|
function asMarkdown(report) {
|
|
const lines = [];
|
|
lines.push('# Lock Reliability Summary');
|
|
lines.push('');
|
|
lines.push(`- Generated at: ${report.generatedAt}`);
|
|
lines.push(`- Window: last ${report.sinceDays} day(s)`);
|
|
lines.push(`- Logs scanned: ${report.logsScanned}`);
|
|
lines.push(`- Runs considered: ${report.runsConsidered}`);
|
|
lines.push('');
|
|
|
|
lines.push('## Totals');
|
|
lines.push('');
|
|
lines.push(`- completed: ${report.statusCounts.completed || 0}`);
|
|
lines.push(`- failed: ${report.statusCounts.failed || 0}`);
|
|
lines.push('');
|
|
|
|
const sections = [
|
|
['By platform', report.byPlatform],
|
|
['By cadence', report.byCadence],
|
|
['By backend error category', report.byBackendCategory],
|
|
['By relay endpoint', report.byRelayEndpoint],
|
|
];
|
|
|
|
for (const [title, obj] of sections) {
|
|
lines.push(`## ${title}`);
|
|
lines.push('');
|
|
const entries = Object.entries(obj);
|
|
if (!entries.length) {
|
|
lines.push('- (none)');
|
|
} else {
|
|
for (const [key, value] of entries) {
|
|
lines.push(`- ${key}: ${value}`);
|
|
}
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
if (report.recurringBackendErrors.length) {
|
|
lines.push('## Recurring backend errors (count >= 2)');
|
|
lines.push('');
|
|
for (const entry of report.recurringBackendErrors) {
|
|
lines.push(`- ${entry.category}: ${entry.count}`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
async function readLogFile(filePath, cadence, cutoffMs) {
|
|
const stat = await fs.stat(filePath);
|
|
if (!stat.isFile() || stat.mtimeMs < cutoffMs) return null;
|
|
|
|
const basename = path.basename(filePath);
|
|
const match = basename.match(/^.+__([^_]+?)__(completed|failed)\.md$/);
|
|
if (!match) return null;
|
|
|
|
const body = await fs.readFile(filePath, 'utf8');
|
|
const frontmatter = parseFrontmatter(body);
|
|
const status = frontmatter.status || match[2];
|
|
const platform = frontmatter.platform || 'unknown';
|
|
const backendCategory = status === 'failed'
|
|
? getBackendCategory({ frontmatter, body })
|
|
: 'n/a';
|
|
const relayEndpoints = collectRelayEndpoints({ frontmatter, body });
|
|
|
|
return {
|
|
filePath,
|
|
cadence: frontmatter.cadence || cadence,
|
|
status,
|
|
platform,
|
|
backendCategory,
|
|
relayEndpoints,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const now = Date.now();
|
|
const cutoffMs = now - (options.sinceDays * 24 * 60 * 60 * 1000);
|
|
|
|
const byPlatform = new Map();
|
|
const byCadence = new Map();
|
|
const byBackendCategory = new Map();
|
|
const byRelayEndpoint = new Map();
|
|
const statusCounts = new Map();
|
|
const records = [];
|
|
let logsScanned = 0;
|
|
|
|
for (const cadence of VALID_CADENCES) {
|
|
const cadenceDir = path.join(options.logRoot, cadence);
|
|
let entries;
|
|
try {
|
|
entries = await fs.readdir(cadenceDir);
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
const promises = entries.map(async (entry) => {
|
|
const fullPath = path.join(cadenceDir, entry);
|
|
const record = await readLogFile(fullPath, cadence, cutoffMs);
|
|
return record;
|
|
});
|
|
|
|
const results = await Promise.all(promises);
|
|
logsScanned += entries.length;
|
|
|
|
for (const record of results) {
|
|
if (!record) continue;
|
|
|
|
records.push(record);
|
|
inc(statusCounts, record.status);
|
|
inc(byPlatform, record.platform);
|
|
inc(byCadence, record.cadence);
|
|
if (record.status === 'failed' && record.backendCategory) {
|
|
inc(byBackendCategory, record.backendCategory);
|
|
}
|
|
for (const relay of record.relayEndpoints) {
|
|
inc(byRelayEndpoint, relay);
|
|
}
|
|
}
|
|
}
|
|
|
|
const recurringBackendErrors = [...byBackendCategory.entries()]
|
|
.filter(([, count]) => count >= 2)
|
|
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
.map(([category, count]) => ({ category, count }));
|
|
|
|
const report = {
|
|
generatedAt: new Date().toISOString(),
|
|
sinceDays: options.sinceDays,
|
|
logRoot: options.logRoot,
|
|
logsScanned,
|
|
runsConsidered: records.length,
|
|
statusCounts: toSortedObject(statusCounts),
|
|
byPlatform: toSortedObject(byPlatform),
|
|
byCadence: toSortedObject(byCadence),
|
|
byBackendCategory: toSortedObject(byBackendCategory),
|
|
byRelayEndpoint: toSortedObject(byRelayEndpoint),
|
|
recurringBackendErrors,
|
|
};
|
|
|
|
await fs.mkdir(options.outDir, { recursive: true });
|
|
const markdown = asMarkdown(report);
|
|
const markdownPath = path.join(options.outDir, 'lock-reliability-summary.md');
|
|
const jsonPath = path.join(options.outDir, 'lock-reliability-summary.json');
|
|
|
|
await fs.writeFile(markdownPath, markdown, 'utf8');
|
|
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
markdown: path.relative(process.cwd(), markdownPath),
|
|
json: path.relative(process.cwd(), jsonPath),
|
|
recurringBackendErrors,
|
|
}));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error?.stack || error?.message || String(error));
|
|
process.exit(1);
|
|
});
|