import fs from "node:fs/promises"; import path from "node:path"; import type { ScannerLearningLoadResult, ScannerLearningRepositoryPort, ScannerLearningRules, ScannerLearningSaveResult } from "./contracts.js"; export class ScannerLearningRepository implements ScannerLearningRepositoryPort { private readonly filePath: string; constructor(userDataPath: string, fileName = "scanner-learning.json") { this.filePath = path.join(userDataPath, fileName); } async load(): Promise { try { const raw = await fs.readFile(this.filePath, "utf8"); const parsed = JSON.parse(raw) as ScannerLearningRules; return { ok: true, path: this.filePath, rules: parsed && typeof parsed === "object" ? parsed : emptyRules(), }; } catch { return { ok: true, path: this.filePath, rules: emptyRules() }; } } async save(rules: ScannerLearningRules): Promise { const current = await this.load(); const payload: ScannerLearningRules = mergeRules(current.rules, rules); await fs.mkdir(path.dirname(this.filePath), { recursive: true }); await fs.writeFile(this.filePath, JSON.stringify(payload, null, 2), "utf8"); return { ok: true, path: this.filePath, rules: payload, total: countRules(payload) }; } } function emptyRules(): ScannerLearningRules { return { textReplacements: {}, fieldAliases: {}, constrainedFixes: {}, cropAdjustments: {}, uiProfileAdjustments: {}, }; } function mergeRules(current: ScannerLearningRules, incoming: ScannerLearningRules): ScannerLearningRules { return { textReplacements: { ...(current.textReplacements ?? {}), ...(incoming.textReplacements ?? {}), }, fieldAliases: mergeNested(current.fieldAliases, incoming.fieldAliases), constrainedFixes: { ...(current.constrainedFixes ?? {}), ...(incoming.constrainedFixes ?? {}), }, cropAdjustments: { ...(current.cropAdjustments ?? {}), ...(incoming.cropAdjustments ?? {}), }, uiProfileAdjustments: { ...(current.uiProfileAdjustments ?? {}), ...(incoming.uiProfileAdjustments ?? {}), }, }; } function mergeNested( current: Record> | undefined, incoming: Record> | undefined, ) { const merged: Record> = {}; for (const [field, values] of Object.entries(current ?? {})) merged[field] = { ...(values ?? {}) }; for (const [field, values] of Object.entries(incoming ?? {})) merged[field] = { ...(merged[field] ?? {}), ...(values ?? {}) }; return merged; } function countRules(rules: ScannerLearningRules) { const fieldAliases = Object.values(rules.fieldAliases ?? {}).reduce((sum, aliases) => sum + Object.keys(aliases ?? {}).length, 0); return ( Object.keys(rules.textReplacements ?? {}).length + fieldAliases + Object.keys(rules.constrainedFixes ?? {}).length + Object.keys(rules.cropAdjustments ?? {}).length + Object.keys(rules.uiProfileAdjustments ?? {}).length ); }