'use strict'; const fs = require('node:fs'); const path = require('node:path'); const crypto = require('node:crypto'); const META_KEYS = ['harvest', 'settings', 'nightly']; class DiscoveryStore { constructor(dataDir) { this.file = path.join(path.resolve(dataDir), 'discovery.json'); } load() { try { const data = JSON.parse(fs.readFileSync(this.file, 'utf8')); if (data.schema !== 1 || !Number.isSafeInteger(data.revision) || !data.candidates || !data.feeds || !data.lastRun) throw new Error('Invalid discovery store schema'); // Phase A files lack the metadata fields; default them. if (data.harvest === undefined) data.harvest = null; if (data.settings === undefined) data.settings = null; if (!data.nightly) data.nightly = { lastDate: null }; return data; } catch (error) { if (error.code === 'ENOENT') return { schema: 1, revision: 0, candidates: {}, feeds: {}, lastRun: { status: 'never' }, harvest: null, settings: null, nightly: { lastDate: null } }; throw new Error(`Discovery store unavailable; refusing to overwrite it: ${error.message}`); } } publish(value) { const prior = this.load(); const data = { schema: 1, revision: prior.revision + 1, candidates: value.candidates, feeds: value.feeds, lastRun: { status: 'success', startedAt: value.startedAt, finishedAt: new Date().toISOString(), candidateCount: Object.keys(value.candidates).length, examined: value.examined, clipped: value.clipped }, harvest: prior.harvest, settings: prior.settings, nightly: prior.nightly }; return this.write(data); } // Metadata only: each given key replaces that field; revision stays. patch(fields) { const data = this.load(); for (const key of META_KEYS) if (key in fields) data[key] = fields[key]; return this.write(data); } write(data) { fs.mkdirSync(path.dirname(this.file), { recursive: true }); const temp = `${this.file}.${crypto.randomBytes(6).toString('hex')}.tmp`; try { fs.writeFileSync(temp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 }); if (fs.existsSync(this.file)) fs.copyFileSync(this.file, `${this.file}.prev`); fs.renameSync(temp, this.file); } finally { try { fs.rmSync(temp, { force: true }); } catch {} } return data; } } module.exports = { DiscoveryStore };