/**
* Resolution — direct storage for learned remediation rules,
* remediation logs, and human overrides (Std 10).
*
* Direct file I/O — no shared persistence layer. Atomic writes.
* Empty by default.
*/
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const STORAGE_DIR = path.resolve(
path.dirname(__filename),
'..', '..', '..', '..', 'data', 'baseline.resolution',
);
export type LearnedSource = 'seed' | 'human' | 'ai' | 'lookup';
export interface LearnedRule {
readonly key: string;
readonly canonical: string;
readonly by: LearnedSource;
readonly at: string;
readonly confidence: number;
readonly citation?: string;
}
export interface RemediationLogEntry {
readonly issueKey: string;
readonly action: 'rule-applied' | 'duplicate-merged' | 'contradiction-resolved' | 'escalated' | 'pass-through';
readonly rationale: string;
readonly at: string;
}
export interface HumanOverrideEntry {
readonly field: string;
readonly value: unknown;
readonly by: string;
readonly at: string;
}
interface RulesFile { readonly version: 1; readonly entries: LearnedRule[]; }
interface LogsFile { readonly version: 1; readonly entries: RemediationLogEntry[]; }
interface OverridesFile { readonly version: 1; readonly entries: HumanOverrideEntry[]; }
export function normalizeKey(s: string): string {
return s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
}
async function readJson<T>(file: string): Promise<T | null> {
try {
const txt = await fs.readFile(file, 'utf8');
return JSON.parse(txt) as T;
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw err;
}
}
async function writeJson(file: string, data: unknown): Promise<void> {
await fs.mkdir(path.dirname(file), { recursive: true });
const tmp = file + '.tmp';
await fs.writeFile(tmp, JSON.stringify(data, null, 2), 'utf8');
await fs.rename(tmp, file);
}
export class ResolutionStore {
private readonly rulesFile: string;
private readonly logsFile: string;
private readonly overridesFile: string;
private rules = new Map<string, LearnedRule>();
private logs: RemediationLogEntry[] = [];
private overrides: HumanOverrideEntry[] = [];
private newlyLearned: LearnedRule[] = [];
private newlyLogged: RemediationLogEntry[] = [];
constructor(dir: string = STORAGE_DIR) {
this.rulesFile = path.join(dir, 'learned-rules.json');
this.logsFile = path.join(dir, 'remediation-logs.json');
this.overridesFile = path.join(dir, 'human-overrides.json');
}
async load(): Promise<void> {
const [r, l, o] = await Promise.all([
readJson<RulesFile>(this.rulesFile),
readJson<LogsFile>(this.logsFile),
readJson<OverridesFile>(this.overridesFile),
]);
if (r) for (const e of r.entries) this.rules.set(e.key, e);
if (l) this.logs.push(...l.entries);
if (o) this.overrides.push(...o.entries);
}
lookupRule(key: string): LearnedRule | undefined {
return this.rules.get(normalizeKey(key));
}
learnRule(rawKey: string, canonical: string, by: LearnedSource, confidence: number, citation?: string): void {
const key = normalizeKey(rawKey);
if (this.rules.has(key)) return;
const e: LearnedRule = { key, canonical, by, at: new Date().toISOString(), confidence, citation };
this.rules.set(key, e);
this.newlyLearned.push(e);
}
logRemediation(entry: RemediationLogEntry): void {
this.logs.push(entry);
this.newlyLogged.push(entry);
}
drainLearned(): LearnedRule[] {
const out = this.newlyLearned;
this.newlyLearned = [];
return out;
}
drainLogged(): RemediationLogEntry[] {
const out = this.newlyLogged;
this.newlyLogged = [];
return out;
}
async save(): Promise<void> {
await Promise.all([
writeJson(this.rulesFile, { version: 1, entries: [...this.rules.values()] } as RulesFile),
writeJson(this.logsFile, { version: 1, entries: this.logs } as LogsFile),
writeJson(this.overridesFile, { version: 1, entries: this.overrides } as OverridesFile),
]);
}
}