BID · Console
Baseline · Intelligence · Decision
src/agents/baseline/resolution/index.ts 11,215 bytes · typescript
/**
 * Resolution agent — runtime entry point.
 *
 * Walks the matrix-defined runbook (Std 6). Most steps are
 * deterministic; LLM is consulted for triage when no rule applies
 * (Std 3 — judgment is explicit, with citation).
 */

import {
  type AgentResult,
  HITL_CONFIDENCE_THRESHOLD,
  type HITLEscalation,
  LOW_CONFIDENCE_THRESHOLD,
  makeConfidence,
  MAX_RECURSION_DEPTH,
} from '../../../standards.js';
import type {
  ExecutionContext,
  FailureObject,
  Handoff,
  Lineage,
  UnresolvedIssue,
} from '../../../types.js';
import { nowIso } from '../../../types.js';
import {
  AGENT_NAME,
  AGENT_VERSION,
  resolutionContract,
} from './matrix.js';
import {
  type LearnedRule as ResolutionLearnedRule,
  type ResolutionInput,
  type ResolutionOutput,
  type ResolvedRecord,
  resolutionInputSchema,
} from './schema.js';
import { ResolutionStore } from './storage.js';
import { triageIssue } from './llm.js';

export { resolutionContract } from './matrix.js';
export type { ResolutionOutput } from './schema.js';

export interface ResolutionSideContext {
  readonly upstreamLineage: Lineage;
}

function trace(ctx: ExecutionContext, standard: number, step: string, detail: string): void {
  ctx.trace.push({ agent: AGENT_NAME, standard, step, detail, at: nowIso() });
  // eslint-disable-next-line no-console
  console.log(`  [${AGENT_NAME}][Std ${standard}] ${step} — ${detail}`);
}

function failure(
  ctx: ExecutionContext,
  category: FailureObject['category'],
  reason: string,
  context: Record<string, unknown>,
  lineage: Lineage,
): FailureObject {
  return {
    agent: AGENT_NAME,
    agentVersion: AGENT_VERSION,
    category,
    reason,
    context,
    lineage,
    attempts: ctx.retries,
    recursionDepth: ctx.recursionDepth,
    occurredAt: nowIso(),
  };
}

export async function runResolution(
  rawInput: unknown,
  side: ResolutionSideContext,
  ctx: ExecutionContext,
): Promise<AgentResult<ResolutionOutput>> {
  /* Std 12: recursion guard. */
  ctx.recursionDepth += 1;
  if (ctx.recursionDepth > MAX_RECURSION_DEPTH) {
    return {
      ok: false,
      escalations: [],
      failure: failure(
        ctx,
        'recursion-limit',
        `Resolution exceeded MAX_RECURSION_DEPTH=${MAX_RECURSION_DEPTH}.`,
        {},
        side.upstreamLineage,
      ),
    };
  }

  /* Step 1 (Std 2): validate-input. */
  const parsed = resolutionInputSchema.safeParse(rawInput);
  if (!parsed.success) {
    return {
      ok: false,
      escalations: [],
      failure: failure(
        ctx,
        'invalid-input',
        'Resolution input failed schema validation.',
        { issues: parsed.error.issues },
        side.upstreamLineage,
      ),
    };
  }
  const input: ResolutionInput = parsed.data;
  trace(ctx, 2, resolutionContract.runbook[0]!.name,
    `reviewing ${input.records.length} record(s) + ${input.unresolvedIssues.length} unresolved issue(s)`);

  /* Std 10: load persistent rules + logs. */
  const store = new ResolutionStore();
  await store.load();

  const records: ResolvedRecord[] = [];
  const stillUnresolved: UnresolvedIssue[] = [];
  const escalations: HITLEscalation[] = [];

  /* Step 3 (Std 3): resolve-conflict — deterministic merge / contradiction handling.
   * Bucket by canonical key; prefer highest confidence. */
  const byKey = new Map<string, typeof input.records>();
  for (const r of input.records) {
    const k = `${r.canonicalEntity}::${r.canonicalMetric}::${r.period}`;
    if (!byKey.has(k)) byKey.set(k, []);
    byKey.get(k)!.push(r);
  }
  for (const [key, group] of byKey) {
    const sorted = [...group].sort((a, b) => b.confidence - a.confidence);
    const primary = sorted[0]!;
    const duplicates = sorted.slice(1);
    const resolutionNotes: string[] = [];
    let action: ResolvedRecord['resolutionAction'] = 'pass-through';

    if (duplicates.length > 0) {
      const hasContradiction = group.some(r => r.flags.includes('contradictory-mapping'));
      if (hasContradiction) {
        action = 'contradiction-resolved';
        resolutionNotes.push(
          `resolved contradiction in ${key}: chose value ${primary.value} from ${primary.sourceUrl} ` +
          `(confidence ${primary.confidence.toFixed(2)}); preserved ${duplicates.length} alternative lineage record(s).`,
        );
        store.learnRule(`prefer:${key}`, primary.sourceUrl, 'ai', primary.confidence,
          'preferred-source rule learned from contradiction resolution');
        store.logRemediation({ issueKey: key, action: 'contradiction-resolved', rationale: resolutionNotes[0]!, at: nowIso() });
      } else {
        action = 'duplicate-merged';
        resolutionNotes.push(`merged ${duplicates.length} duplicate(s) into the primary record.`);
        store.logRemediation({ issueKey: key, action: 'duplicate-merged', rationale: resolutionNotes[0]!, at: nowIso() });
      }
    } else if (primary.confidence < LOW_CONFIDENCE_THRESHOLD) {
      action = 'rule-applied';
      resolutionNotes.push('low-confidence record retained with flag; no destructive overwrite (Std 4).');
    }

    /* Step 5 (Std 7): rescore confidence after remediation. */
    const rescored =
      action === 'contradiction-resolved' ? Math.min(0.85, primary.confidence + 0.05)
      : action === 'duplicate-merged' ? Math.min(0.95, primary.confidence + 0.05)
      : primary.confidence;

    records.push({
      ...primary,
      confidence: rescored,
      resolutionAction: action,
      resolutionNotes,
      flags: primary.flags.filter(f => f !== 'contradictory-mapping'),
    });
  }
  trace(ctx, 3, resolutionContract.runbook[2]!.name,
    `${records.length} record(s) packaged after conflict resolution`);

  /* Step 2 (Std 3): non-record issues — triage via LLM if available, otherwise
   * escalate deterministically per the matrix (Std 9 — unresolved issue, no
   * approved rule). */
  for (const issue of input.unresolvedIssues) {
    const rule = store.lookupRule(`triage:${issue.category}`);
    if (rule) {
      store.logRemediation({ issueKey: issue.category, action: 'rule-applied', rationale: `learned rule "${rule.canonical}"`, at: nowIso() });
      stillUnresolved.push(issue);
      continue;
    }
    const t = await triageIssue({ category: issue.category, detail: issue.detail, context: issue.context });
    if (t.ok) {
      store.logRemediation({ issueKey: issue.category, action: t.value.remediationPath === 'hitl' ? 'escalated' : 'rule-applied', rationale: t.value.rationale, at: nowIso() });
      store.learnRule(`triage:${issue.category}`, t.value.remediationPath, 'ai', t.value.confidence, t.value.rationale);
      if (t.value.remediationPath === 'hitl' || t.value.confidence < HITL_CONFIDENCE_THRESHOLD) {
        stillUnresolved.push(issue);
        escalations.push({
          agent: AGENT_NAME,
          reason: issue.category,
          failureContext: `${issue.detail}; recommended: ${t.value.recommendedAction}`,
          lineage: side.upstreamLineage,
          validation: {
            status: 'review',
            confidence: makeConfidence(t.value.confidence, t.value.rationale),
            checks: [{ name: 'remediation-attempted', passed: false }],
          },
          recommendedReviewer: t.value.severity === 'critical' ? 'data-steward' : 'domain-expert',
          raisedAt: nowIso(),
        });
      }
    } else {
      /* No LLM available → preserve issue + escalate (Std 12). */
      stillUnresolved.push(issue);
      store.logRemediation({ issueKey: issue.category, action: 'escalated', rationale: `LLM unavailable: ${t.failure.reason}`, at: nowIso() });
      escalations.push({
        agent: AGENT_NAME,
        reason: issue.category,
        failureContext: `${issue.detail} (LLM unavailable: ${t.failure.reason})`,
        lineage: side.upstreamLineage,
        validation: {
          status: 'review',
          confidence: makeConfidence(0, 'LLM unavailable, no learned rule'),
          checks: [{ name: 'remediation-attempted', passed: false }],
        },
        recommendedReviewer: issue.category === 'failed-retrieval' ? 'data-steward' : 'domain-expert',
        raisedAt: nowIso(),
      });
    }
  }
  trace(ctx, 9, resolutionContract.runbook[1]!.name,
    `triaged ${input.unresolvedIssues.length} issue(s); ${escalations.length} escalation(s) raised`);

  /* Std 10: drain new rules + logs; persist. */
  const learned = store.drainLearned();
  const newRules: ResolutionLearnedRule[] = learned.filter(e => e.by !== 'seed').map(e => ({ key: e.key, value: e.canonical }));
  if (learned.length > 0 || store.drainLogged().length > 0) await store.save();
  trace(ctx, 10, resolutionContract.runbook[5]!.name,
    `${newRules.length} learned rule(s) for write-back`);

  /* Step 4 (Std 7): revalidate. */
  const blocking = stillUnresolved.filter(u => u.blocking).length;
  const avgConf = records.length === 0
    ? 0
    : records.reduce((s, r) => s + r.confidence, 0) / records.length;
  const confidence = makeConfidence(
    Math.max(0, avgConf - 0.15 * blocking),
    `avg resolved-record confidence ${avgConf.toFixed(2)} with ${blocking} blocking residual issue(s)`,
  );
  trace(ctx, 7, resolutionContract.runbook[3]!.name,
    `revalidated; agent-level confidence ${confidence.tier} (${confidence.value.toFixed(2)})`);

  if (records.length === 0 && stillUnresolved.length > 0) {
    return {
      ok: false,
      escalations,
      failure: failure(
        ctx,
        'unresolved-remediation',
        'No records survived resolution; all issues escalated.',
        { stillUnresolved },
        side.upstreamLineage,
      ),
    };
  }

  /* Step 6 (Std 11): handoff — resolved dataset or escalation package. */
  const lineage: Lineage = {
    sourceUrl: side.upstreamLineage.sourceUrl,
    capturedAt: nowIso(),
    effectiveAs: side.upstreamLineage.effectiveAs,
    agentVersion: AGENT_VERSION,
    upstream: Array.from(new Set([...side.upstreamLineage.upstream, ...records.map(r => r.sourceUrl)])),
  };
  const validationStatus =
    blocking > 0 || stillUnresolved.length > 0
      ? 'review'
      : confidence.value < LOW_CONFIDENCE_THRESHOLD
        ? 'flagged'
        : 'passed';

  const handoff: Handoff<ResolutionOutput> = {
    fromAgent: AGENT_NAME,
    fromAgentVersion: AGENT_VERSION,
    toAgent: null,
    payload: { records, stillUnresolved, learnedRules: newRules },
    metadata: {
      analysisId: ctx.analysisId,
      capabilities: resolutionContract.capabilities,
      recursionDepth: ctx.recursionDepth,
    },
    confidence,
    validation: {
      status: validationStatus,
      checks: [
        { name: 'all-records-resolved', passed: records.every(r => !r.flags.includes('contradictory-mapping')) },
        { name: 'no-blocking-residual', passed: blocking === 0, detail: `${blocking} blocking` },
        { name: 'lineage-preserved', passed: records.every(r => r.sourceUrl.length > 0) },
      ],
    },
    unresolvedIssues: stillUnresolved,
    lineage,
    timestamp: nowIso(),
  };
  trace(ctx, 11, resolutionContract.runbook[5]!.name,
    `baseline pillar complete; ${records.length} resolved, ${stillUnresolved.length} escalated`);

  return { ok: true, handoff, escalations };
}