BID · Console
Baseline · Intelligence · Decision
src/agents/baseline/resolution/storage.ts 4,189 bytes · typescript
/**
 * 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),
    ]);
  }
}