Web UI
Contracts

Web UI Contracts — TypeScript Interface Documentation

Last updated: Feb 20, 2026 21:20 UTC
Location: ui/contracts/
Purpose: Canonical payload structure + adapter system + domain plugin interfaces


Overview

The Vitruvyan Web UI contract system defines 3 core TypeScript interfaces that enforce separation of concerns between backend, adapters, and UI:

ContractPurposeFile
UIContractCanonical payload consumed by rendererUIContract.ts (195 LOC)
AdapterContractBackend state → UI transformationAdapterContract.ts (260 LOC)
DomainPluginContractDomain extension mechanismDomainPluginContract.ts (274 LOC)

Total: 820 lines of TypeScript


1. UIContract — Canonical Payload Structure

Purpose

Defines the single source of truth for all UI payloads consumed by VitruvyanResponseRenderer.

File

ui/contracts/UIContract.ts (195 lines)

Core Interface: UIResponsePayload

export interface UIResponsePayload {
  narrative: NarrativeBlock | null;
  followUps: FollowUpChips | null;
  evidence: EvidenceSection[] | null;
  vee_explanations: VEEExplanations | null;
  context: ContextMetadata | null;
}

Sub-Interfaces

NarrativeBlock

High-level summary with optional VEE annotation.

export interface NarrativeBlock {
  text: string;               // Markdown-formatted summary (100-300 words)
  vee_key?: string;           // Optional VEE key for explainability
  intent_badge?: IntentBadge; // Intent classification display
}

Example:

{
  text: "Apple Inc. (AAPL) shows strong fundamentals with a Solidità score of +2.3σ above sector median.",
  vee_key: "vee_summary_finance_ticker",
  intent_badge: { label: "Finance Analysis", color: "green" }
}

FollowUpChips

Interactive follow-up suggestion chips.

export interface FollowUpChips {
  chips: Array<{
    text: string;
    action?: "query" | "navigate" | "drill_down";
    payload?: any;
  }>;
}

Example:

{
  chips: [
    { text: "Compare with sector peers", action: "query", payload: { sector: "tech" } },
    { text: "Historical trend analysis", action: "drill_down" },
    { text: "Show risk factors", action: "navigate", payload: { section: "risk" } }
  ]
}

EvidenceSection

Collapsible accordion with metric cards.

export interface EvidenceSection {
  title: string;              // Section title (e.g., "Solidità — Balance Sheet")
  subtitle?: string;          // Optional subtitle
  cards: MetricCard[];        // Array of metric cards
  vee_key?: string;           // Optional VEE for section-level explanation
  epistemic_order?: number;   // Epistemological priority (1 = highest)
}
 
export interface MetricCard {
  label: string;              // Metric name (e.g., "Debt-to-Equity Ratio")
  value: string | number;     // Display value (e.g., "0.45" or "$12.5B")
  unit?: string;              // Optional unit (e.g., "%", "B", "x")
  trend?: "up" | "down" | "neutral"; // Trend indicator
  severity?: "positive" | "negative" | "neutral"; // Color coding
  vee_key?: string;           // VEE key for metric-level explanation
  metadata?: Record<string, any>; // Arbitrary metadata
}

Example:

{
  title: "Solidità — Balance Sheet Strength",
  subtitle: "Leverage and liquidity metrics",
  epistemic_order: 1,
  vee_key: "vee_solidita_section",
  cards: [
    {
      label: "Debt-to-Equity",
      value: 0.45,
      unit: "x",
      trend: "down",
      severity: "positive",
      vee_key: "vee_debt_equity_ratio"
    },
    {
      label: "Current Ratio",
      value: 2.1,
      unit: "x",
      trend: "up",
      severity: "positive",
      vee_key: "vee_current_ratio"
    }
  ]
}

VEEExplanations

Three-level explainability registry.

export interface VEEExplanations {
  [vee_key: string]: {
    technical: string;        // 5-15s read (for engineers)
    detailed: string;         // 30-60s read (for analysts)
    contextualized: string;   // 120-180s read (for domain experts)
  };
}

Example:

{
  "vee_debt_equity_ratio": {
    technical: "Ratio of total liabilities to shareholder equity. Formula: Total Debt / Total Equity.",
    detailed: "Measures financial leverage. Lower values indicate conservative capital structure. Sector median is 0.8x.",
    contextualized: "A 0.45x ratio suggests the company is underleveraged compared to peers. This provides flexibility in economic downturns but may indicate underutilized capital. Cross-reference with ROE and WACC to assess capital efficiency."
  }
}

ContextMetadata

Additional context for the response.

export interface ContextMetadata {
  intent?: string;
  domain?: string;
  conversation_id?: string;
  timestamp?: string;
  advisor?: {
    text: string;
    severity: "info" | "warning" | "critical";
  };
  debug?: Record<string, any>;
}

Example:

{
  intent: "finance_single_ticker",
  domain: "finance",
  conversation_id: "conv_1234",
  timestamp: "2026-02-20T21:00:00Z",
  advisor: {
    text: "This analysis uses data as of market close 2026-02-19. Macroeconomic events may impact valuations.",
    severity: "info"
  }
}

Exported Types

export type {
  UIResponsePayload,
  NarrativeBlock,
  FollowUpChips,
  EvidenceSection,
  MetricCard,
  VEEExplanations,
  ContextMetadata,
  IntentBadge
};

2. AdapterContract — Transformation Interface

Purpose

Defines the adapter interface that transforms backend LangGraphFinalState into UIResponsePayload.

File

ui/contracts/AdapterContract.ts (260 lines)

Core Abstract Class: BaseAdapter

export abstract class BaseAdapter {
  abstract conversationType: string;
 
  /**
   * Check if this adapter should handle the conversation
   */
  abstract match(conversation: ConversationType): boolean;
 
  /**
   * Transform backend state into UI payload
   */
  abstract map(state: LangGraphFinalState): UIResponsePayload;
 
  /**
   * Helper: Build narrative block
   */
  protected buildNarrative(
    text: string,
    vee_key?: string,
    intent_badge?: IntentBadge
  ): NarrativeBlock {
    return { text, vee_key, intent_badge };
  }
 
  /**
   * Helper: Build follow-up chips
   */
  protected buildFollowUps(chips: string[]): FollowUpChips {
    return {
      chips: chips.map(text => ({ text, action: "query" }))
    };
  }
 
  /**
   * Helper: Build evidence section
   */
  protected buildEvidenceSection(
    title: string,
    cards: MetricCard[],
    options?: {
      subtitle?: string;
      vee_key?: string;
      epistemic_order?: number;
    }
  ): EvidenceSection {
    return { title, cards, ...options };
  }
 
  /**
   * Helper: Build VEE explanations
   */
  protected buildVEE(
    key: string,
    technical: string,
    detailed: string,
    contextualized: string
  ): VEEExplanations {
    return { [key]: { technical, detailed, contextualized } };
  }
 
  /**
   * Helper: Build context metadata
   */
  protected buildContext(state: LangGraphFinalState): ContextMetadata {
    return {
      intent: state.intent,
      domain: state.domain,
      conversation_id: state.conversation_id,
      timestamp: new Date().toISOString()
    };
  }
}

Adapter Registry Interface

export interface AdapterRegistry {
  /**
   * Register an adapter
   */
  register(adapter: BaseAdapter): void;
 
  /**
   * Select the appropriate adapter for a conversation
   */
  selectAdapter(conversation: ConversationType): BaseAdapter;
 
  /**
   * List all registered adapters
   */
  list(): BaseAdapter[];
}

Implementation: AdapterRegistryImpl

class AdapterRegistryImpl implements AdapterRegistry {
  private adapters: BaseAdapter[] = [];
 
  register(adapter: BaseAdapter): void {
    this.adapters.push(adapter);
  }
 
  selectAdapter(conversation: ConversationType): BaseAdapter {
    for (const adapter of this.adapters) {
      if (adapter.match(conversation)) {
        return adapter;
      }
    }
    
    // Fallback to ConversationalAdapter
    return new ConversationalAdapter();
  }
 
  list(): BaseAdapter[] {
    return [...this.adapters];
  }
}
 
export const adapterRegistry: AdapterRegistry = new AdapterRegistryImpl();

Usage Example

import { BaseAdapter, adapterRegistry } from '@/contracts/AdapterContract';
 
class MyAdapter extends BaseAdapter {
  conversationType = "my_intent";
 
  match(conversation) {
    return conversation.intent === "my_intent";
  }
 
  map(state) {
    return {
      narrative: this.buildNarrative("Summary text", "vee_summary"),
      followUps: this.buildFollowUps(["Question 1?", "Question 2?"]),
      evidence: [
        this.buildEvidenceSection("Section 1", [
          { label: "Metric A", value: 123, unit: "x", vee_key: "vee_metric_a" }
        ])
      ],
      vee_explanations: this.buildVEE(
        "vee_summary",
        "Technical explanation",
        "Detailed explanation",
        "Contextualized explanation"
      ),
      context: this.buildContext(state)
    };
  }
}
 
// Register at app boot
adapterRegistry.register(new MyAdapter());
 
// Use in component
const adapter = adapterRegistry.selectAdapter(conversation);
const payload = adapter.map(state);

3. DomainPluginContract — Extension Mechanism

Purpose

Enables domain-specific extensions (adapters, VEE content, hooks, theme overrides) without modifying core UI code.

File

ui/contracts/DomainPluginContract.ts (274 lines)

Core Interface: DomainPlugin

export interface DomainPlugin {
  metadata: {
    id: string;              // Unique plugin ID (e.g., "finance-ui")
    domain: string;          // Domain name (e.g., "finance", "energy")
    version: string;         // Semantic version (e.g., "1.0.0")
    description?: string;    // Optional description
  };
 
  adapters?: BaseAdapter[];  // Domain-specific adapters
  vee_content?: VEEExplanations; // Default VEE content for domain
  hooks?: Record<string, any>; // Custom React hooks
  theme_overrides?: {
    colors?: Record<string, string>;
    spacing?: Record<string, any>;
    radius?: Record<string, number>;
  };
}

Domain Plugin Registry Interface

export interface DomainPluginRegistry {
  /**
   * Register a domain plugin
   */
  register(plugin: DomainPlugin): void;
 
  /**
   * Get plugin by ID
   */
  getPlugin(id: string): DomainPlugin | undefined;
 
  /**
   * Get all plugins for a domain
   */
  getPluginsByDomain(domain: string): DomainPlugin[];
 
  /**
   * List all registered plugins
   */
  list(): DomainPlugin[];
}

Implementation: DomainPluginRegistryImpl

class DomainPluginRegistryImpl implements DomainPluginRegistry {
  private plugins: Map<string, DomainPlugin> = new Map();
 
  register(plugin: DomainPlugin): void {
    this.plugins.set(plugin.metadata.id, plugin);
    
    // Auto-register adapters
    if (plugin.adapters) {
      plugin.adapters.forEach(adapter => {
        adapterRegistry.register(adapter);
      });
    }
  }
 
  getPlugin(id: string): DomainPlugin | undefined {
    return this.plugins.get(id);
  }
 
  getPluginsByDomain(domain: string): DomainPlugin[] {
    return Array.from(this.plugins.values())
      .filter(p => p.metadata.domain === domain);
  }
 
  list(): DomainPlugin[] {
    return Array.from(this.plugins.values());
  }
}
 
export const domainPluginRegistry: DomainPluginRegistry = new DomainPluginRegistryImpl();

Usage Example: Finance Plugin

import { domainPluginRegistry } from '@/contracts/DomainPluginContract';
import { FinanceSingleTickerAdapter } from './adapters/FinanceSingleTickerAdapter';
import { useTradingOrder, usePortfolioCanvas } from './hooks';
 
const financePlugin: DomainPlugin = {
  metadata: {
    id: 'finance-ui',
    domain: 'finance',
    version: '1.0.0',
    description: 'Finance vertical UI extensions'
  },
 
  adapters: [
    new FinanceSingleTickerAdapter(),
    new FinanceScreeningAdapter(),
    new FinancePortfolioAdapter()
  ],
 
  vee_content: {
    "vee_pe_ratio": {
      technical: "Price-to-Earnings ratio. Formula: Market Cap / Net Income.",
      detailed: "Valuation multiple. Sector median is 18x. Higher = market expects growth.",
      contextualized: "P/E ratios vary by industry. Tech companies trade at 25-40x due to growth expectations. Utilities trade at 10-15x due to stable cash flows. Compare to PEG ratio for growth-adjusted valuation."
    }
  },
 
  hooks: {
    useTradingOrder,
    usePortfolioCanvas
  },
 
  theme_overrides: {
    colors: {
      primary: '#10b981', // Green for finance
      accent: '#3b82f6'
    }
  }
};
 
// Register at app boot (e.g., in app/layout.tsx)
domainPluginRegistry.register(financePlugin);

Contract Enforcement

Type Safety

All contracts are TypeScript interfaces enforced at compile time:

// ✅ GOOD: Conforms to UIResponsePayload
const payload: UIResponsePayload = {
  narrative: { text: "Summary", vee_key: "vee_summary" },
  followUps: { chips: [{ text: "Question?" }] },
  evidence: null,
  vee_explanations: null,
  context: null
};
 
// ❌ ERROR: Missing required field
const badPayload: UIResponsePayload = {
  narrative: { text: "Summary" }
  // Missing: followUps, evidence, vee_explanations, context
};

Runtime Validation

Use type guards for runtime validation:

export function isUIResponsePayload(obj: any): obj is UIResponsePayload {
  return (
    obj !== null &&
    typeof obj === 'object' &&
    ('narrative' in obj || 'followUps' in obj || 'evidence' in obj)
  );
}
 
// Usage
if (isUIResponsePayload(payload)) {
  // Safe to use payload
}

Export Index

All contracts exported via ui/contracts/index.ts:

// ui/contracts/index.ts
export type {
  UIResponsePayload,
  NarrativeBlock,
  FollowUpChips,
  EvidenceSection,
  MetricCard,
  VEEExplanations,
  ContextMetadata
} from './UIContract';
 
export {
  BaseAdapter,
  type AdapterRegistry,
  adapterRegistry
} from './AdapterContract';
 
export {
  type DomainPlugin,
  type DomainPluginRegistry,
  domainPluginRegistry
} from './DomainPluginContract';

Contract Versioning

Contracts follow semantic versioning:

  • Major (1.0.0 → 2.0.0): Breaking changes (field removal/rename)
  • Minor (1.0.0 → 1.1.0): New fields (backward compatible)
  • Patch (1.0.0 → 1.0.1): Documentation/fixes (no interface changes)

Current version: 1.0.0 (initial release, Feb 2026)


Migration Guide

From Legacy to Contracts (Pre-Feb 2026 → v1.0.0)

Legacy PatternNew Contract Pattern
Component reads state.summaryAdapter produces narrative: { text: state.summary }
Component calculates metricsAdapter populates evidence: [{ cards: [...] }]
Hardcoded VEE in componentAdapter provides vee_explanations: { ... }
Intent-based renderingAdapter registry selects adapter by match()

References


Last updated: Feb 20, 2026 21:20 UTC