BID · Console
Baseline · Intelligence · Decision
src/decision/channel-registry.ts 4,817 bytes · typescript
/**
 * Decision — channel-adapter registry.
 *
 * Per Pillar 3 spec §SME Rule Library, delivery rules reference
 * channel adapters by name rather than hardcoding email / Slack /
 * dashboards in framework code. Organizations register their specific
 * channels at bootstrap; the framework only ships the universal
 * `audit-log` channel, a no-side-effect adapter that records a
 * "would-deliver" event to the audit trail. Real channels are added
 * by deployers without touching the framework — same additive pattern
 * as src/tools/retrieval/connectors/.
 *
 * Std 4 (Decision): no delivery outside approved channels — if a
 * delivery rule names a channel that isn't registered, the Delivery
 * & Distribution agent fails the dispatch and escalates per Std 9.
 *
 * Std 12: every dispatch result is structured; channels never throw
 * raw exceptions across the boundary.
 */

export interface DispatchPayload {
  /** Free-text recipient identifier (audience tier descriptor, not a
   *  PII address). The channel adapter is responsible for translating
   *  this into the channel-specific addressing, if any. */
  readonly recipient: string;
  readonly audienceTier: string;
  /** Inline content the channel should deliver. */
  readonly content: { readonly kind: string; readonly data: unknown };
  /** Reference back to the upstream visualization / recommendation
   *  the dispatch is for, so the audit trail can reconstruct lineage. */
  readonly contentReference: string;
  /** Caller-supplied severity (low | normal | material | high_impact). */
  readonly severity?: string;
  /** Whether the channel must request an acknowledgment back. */
  readonly acknowledgmentRequired?: boolean;
  /** Optional ack window in seconds (rule-driven; the channel decides
   *  whether to enforce). */
  readonly acknowledgmentWindowSec?: number;
}

export interface DispatchOutcome {
  readonly ok: boolean;
  readonly channel: string;
  readonly dispatchedAt: string;
  readonly channelMessage?: string;
  readonly acknowledgmentRequired: boolean;
  readonly acknowledgmentState: 'pending' | 'acknowledged' | 'not-applicable' | 'failed';
  readonly error?: { readonly category: string; readonly message: string };
}

export interface Channel {
  readonly name: string;
  readonly description: string;
  dispatch(payload: DispatchPayload): Promise<DispatchOutcome>;
}

const registry = new Map<string, Channel>();

export function registerChannel(channel: Channel): void {
  registry.set(channel.name, channel);
}

export function unregisterChannel(name: string): void {
  registry.delete(name);
}

export function listChannels(): readonly string[] {
  return [...registry.keys()].sort();
}

export function getChannel(name: string): Channel | null {
  return registry.get(name) ?? null;
}

/* -------------------------------------------------------------- *
 * Built-in: audit-log channel
 *
 * The framework's default no-side-effect channel. Logs the dispatch
 * to the audit trail (via console + an in-memory buffer the orchestrator
 * persists). Real channels (email, secure-message, dashboard, etc.)
 * are registered by deployers at bootstrap.
 * -------------------------------------------------------------- */

interface AuditLogEntry {
  readonly at: string;
  readonly recipient: string;
  readonly audienceTier: string;
  readonly contentReference: string;
  readonly severity: string;
  readonly contentKind: string;
}

const auditBuffer: AuditLogEntry[] = [];

export function drainAuditLog(): readonly AuditLogEntry[] {
  const snapshot = [...auditBuffer];
  auditBuffer.length = 0;
  return snapshot;
}

export function peekAuditLog(): readonly AuditLogEntry[] {
  return [...auditBuffer];
}

class AuditLogChannel implements Channel {
  readonly name = 'audit-log';
  readonly description = 'Framework default: records a would-deliver event to the audit trail. No external side effects.';

  async dispatch(payload: DispatchPayload): Promise<DispatchOutcome> {
    const at = new Date().toISOString();
    auditBuffer.push({
      at,
      recipient: payload.recipient,
      audienceTier: payload.audienceTier,
      contentReference: payload.contentReference,
      severity: payload.severity ?? 'normal',
      contentKind: payload.content.kind,
    });
    return {
      ok: true,
      channel: this.name,
      dispatchedAt: at,
      channelMessage: `audit-log recorded dispatch for ${payload.audienceTier} (ref=${payload.contentReference})`,
      acknowledgmentRequired: payload.acknowledgmentRequired ?? false,
      acknowledgmentState: payload.acknowledgmentRequired ? 'pending' : 'not-applicable',
    };
  }
}

/* Self-register the default at module load — same pattern as
 * src/tools/retrieval/connectors/ register their connectors. */
registerChannel(new AuditLogChannel());