Skip to content
/ solutions / agnostic-buyer-rep-compliance-agent Buyer-Rep Compliance Agent for Small Brokerages Automate buyer-rep agreement paper trails and compliance checks.
The problem As a broker-owner, you're responsible for ensuring every buyer-rep agreement is signed, stored, and compliant with state regulations. Your agents often forget to upload contracts, miss disclosure deadlines, or use outdated forms. Manual audits are time-consuming and error-prone, exposing your firm to legal risk and fines. You need a system that tracks agreement lifecycle and alerts you to gaps.
Example artifact A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
166 kB · 72 tests· 96.7% coverage· vitest passing
SHA-256 7139afd9b359ece10ffe9083b2ade3bb4094b349a9ad68351330620e4688e65d Comments Sign in to commentSign in with GitHub to comment and vote.
Las Vegas, Nevada USA © 2026 REAA Technologies Inc. — Open-Source AI Solutions for Small Business.
On this page Intro
This tutorial walks through building a Buyer-Rep Compliance Agent for small brokerages — a Next.js application that automates buyer-rep agreement management, compliance checks, and RAG-powered Q&A. You’ll wire up six REAA packages (agent-memory, agent-memory-storage, agent-memory-retrieval, rag-eval-core, rag-eval-dataset, rag-eval-gate) into a Hono API with an end-to-end test suite.
By the end, you’ll have a working compliance dashboard that tracks agreements, runs five regulatory rules against each one, stores findings in an agent memory store, answers questions via a retrieval-augmented generation pipeline, and evaluates RAG quality with CI-friendly gates.
Prerequisites
Node.js 22+ and pnpm 10+ installed
An OpenAI API key (for embeddings and LLM calls)
A Langfuse account (optional — used for observability)
Basic familiarity with Next.js App Router, TypeScript, and Hono
Step 1: Create the project and install dependencies
Start a new Next.js project and add the dependencies you’ll need. The package.json below pins every package to an exact version — no ^ or ~ ranges.
{
"name" : "agnostic-buyer-rep-compliance-agent" ,
"version" : "0.1.0" ,
"private" : true ,
"scripts" : {
"dev" : "next dev" ,
"build" : "next build" ,
"start" : "next start" ,
"lint" : "eslint ." ,
"typecheck" : "tsc --noEmit" ,
"test" : "vitest run --coverage --reporter=json --outputFile=vitest-report.json"
},
"dependencies" : {
"@reaatech/agent-memory" : "0.1.0" ,
"@reaatech/agent-memory-retrieval" : "0.1.0" ,
"@reaatech/agent-memory-storage" : "0.1.0" ,
"@reaatech/rag-eval-core" : "0.1.0" ,
"@reaatech/rag-eval-dataset" : "0.1.0" ,
"@reaatech/rag-eval-gate" : "0.1.0" ,
"ai" : "6.0.208" ,
"@ai-sdk/openai" : "3.0.73" ,
"hono" : "4.12.26" ,
"langfuse" : "3.38.20" ,
"next" : "16.2.9" ,
"react" : "19.2.4" ,
"react-dom" : "19.2.4" ,
"zod" : "4.4.3"
},
"devDependencies" : {
"@types/node" : "20.19.43" ,
"@types/react" : "19.2.17" ,
"@types/react-dom" : "19.2.3" ,
"@vitest/coverage-v8" : "4.1.9" ,
"eslint" : "9.39.4" ,
"eslint-config-next" : "16.2.9" ,
"msw" : "2.14.6" ,
"typescript" : "5.9.3" ,
"typescript-eslint" : "8.61.1" ,
"vitest" : "4.1.9"
},
"type" : "module" ,
"engines" : {
"node" : ">=22"
},
"packageManager" : "pnpm@10.0.0" ,
"license" : "MIT"
} Expected output: pnpm resolves all 16+ dependencies and writes a pnpm-lock.yaml.
Step 2: Define domain types with Zod schemas Create src/lib/types.ts with Zod schemas for agreements, disclosures, compliance results, and conversation turns. These schemas provide runtime validation and inferred TypeScript types.
import { z } from "zod" ;
export const AgreementStatus = z. enum ([ "draft" , "sent" , "signed" , "expiring" , "expired" , "archived" ]);
export type AgreementStatus = z . infer < typeof AgreementStatus>;
export const DisclosureType = z. enum ([ "buyer-broker-disclosure" , "agency-disclosure" , "lead-based-paint" , "property-disclosure" , "other" ]);
export type DisclosureType = z . infer < typeof DisclosureType>;
export const DisclosureSchema = z. object ({
id: z. string (),
type: DisclosureType,
title: z. string (),
signedAt: z.coerce. date (). optional (),
required: z. boolean (),
fileUrl: z. string (). optional (),
});
export type Disclosure = z . infer < typeof DisclosureSchema>;
export const AgreementSchema = z. object ({
id: z. string (),
propertyAddress: z. string (),
buyerName: z. string (),
agentName: z. string (),
brokerName: z. string (),
status: AgreementStatus,
createdAt: z.coerce. date (),
signedAt: z.coerce. date (). optional (),
expiresAt: z.coerce. date (),
disclosures: z. array (DisclosureSchema),
metadata: z. record (z. string (), z. unknown ()). optional (),
});
export type Agreement = z . infer < typeof AgreementSchema>;
export interface ComplianceFinding {
ruleId : string ;
passed : boolean ;
message : string ;
severity : "critical" | "warning" | "info" ;
suggestedFix ?: string ;
}
export interface ComplianceRule {
id : string ;
code : string ;
description : string ;
category : string ;
severity : "critical" | "warning" | "info" ;
validate (agreement : Agreement ) : ComplianceFinding ;
}
export interface ComplianceCheckResult {
agreementId : string ;
checkedAt : Date ;
findings : ComplianceFinding [];
overallStatus : "pass" | "fail" | "warn" ;
score : number ;
}
export interface ConversationTurn {
speaker : "user" | "agent" | "system" ;
content : string ;
timestamp : Date ;
}
Step 3: Create runtime configuration Create src/lib/config.ts that reads environment variables, validates them through a Zod schema, and exports a lazy singleton proxy.
import { z } from "zod" ;
const AppConfigSchema = z. object ({
openaiApiKey: z. string (),
modelName: z. string (),
embeddingModel: z. string (),
langfusePublicKey: z. string (),
langfuseSecretKey: z. string (),
langfuseHost: z. string (),
nodeEnv: z. string (),
});
type AppConfig = z . infer < typeof AppConfigSchema>;
let _config : AppConfig | null = null ;
export function loadConfig () : AppConfig {
const raw = {
openaiApiKey: process.env.OPENAI_API_KEY ?? "" ,
modelName: process.env.MODEL_NAME ?? "gpt-5.2-mini" ,
embeddingModel: process.env.EMBEDDING_MODEL ?? "text-embedding-3-small" ,
langfusePublicKey: process.env.LANGFUSE_PUBLIC_KEY ?? "" ,
langfuseSecretKey: process.env.LANGFUSE_SECRET_KEY ?? "" ,
langfuseHost: process.env.LANGFUSE_HOST ?? "https://cloud.langfuse.com" ,
nodeEnv: ( "development" as string ),
};
return AppConfigSchema. parse (raw);
}
export const config : AppConfig = new Proxy ({} as AppConfig , {
get (_target, prop : keyof AppConfig ) {
if ( ! _config) {
_config = loadConfig ();
}
return _config[prop];
},
}); Populate .env.example with all required environment variables:
# Env vars used by agnostic-buyer-rep-compliance-agent.
# The builder adds entries here as it wires up each integration.
# Keep placeholders only — never commit real values.
NODE_ENV=development
OPENAI_API_KEY=<your-openai-key>
MODEL_NAME=gpt-5.2-mini
EMBEDDING_MODEL=text-embedding-3-small
LANGFUSE_PUBLIC_KEY=<your-langfuse-pk>
LANGFUSE_SECRET_KEY=<your-langfuse-sk>
LANGFUSE_HOST=https://cloud.langfuse.com
Step 4: Set up Langfuse observability Create a logger wrapper in src/lib/logger.ts that delegates to the Langfuse SDK:
import { Langfuse } from "langfuse" ;
class Logger {
private client : Langfuse | null = null ;
private defaultTraceId = "default" ;
init (publicKey : string , secretKey : string , host : string ) : void {
this.client = new Langfuse ({
publicKey,
secretKey,
baseUrl: host,
});
}
async trace < T >(name : string , fn : () => Promise < T >) : Promise < T > {
if ( ! this.client) return fn ();
const trace = this.client. trace ({ id: this.defaultTraceId, name });
try {
const result = await fn ();
return result;
} finally {
trace. update ({ name, metadata: { completed: true } });
}
}
async span < T >(name : string , fn : () => Promise < T >) : Promise < T > {
if ( ! this.client) return fn ();
const trace = this.client. trace ({ id: this.defaultTraceId, name });
const span = trace. span ({ name });
try {
const result = await fn ();
return result;
} finally {
span. end ();
}
}
score (traceId : string , name : string , value : number ) : void {
if ( ! this.client) return ;
this.client. score ({ traceId, name, value });
}
}
export const logger = new Logger (); Next, create src/instrumentation.ts that initializes the logger at server startup. This file fires once when the Next.js server boots, but only in the Node.js runtime (not Edge):
/* v8 ignore next 8 */
export async function register () {
if (process.env.NEXT_RUNTIME === "nodejs" ) {
const { logger } = await import ( "./lib/logger" );
const { config } = await import ( "./lib/config" );
logger. init (config.langfusePublicKey, config.langfuseSecretKey, config.langfuseHost);
}
} Finally, enable the instrumentation hook in next.config.ts:
import type { NextConfig } from "next" ;
const nextConfig = {
experimental: {
instrumentationHook: true ,
},
} as NextConfig ;
export default nextConfig; Expected output: The register() function fires once during server boot. Langfuse is ready before any request arrives.
Step 5: Build the agreement service Create src/services/agreement-service.ts — an in-memory CRUD service backed by a Map<string, Agreement>. It validates every write through the Zod schema.
import { AgreementSchema, AgreementStatus } from "../lib/types" ;
import type { Agreement } from "../lib/types" ;
export class AgreementService {
private store = new Map < string , Agreement >();
create (data : Omit < Agreement , "id" | "status" | "createdAt" >) : Agreement {
const agreement : Agreement = AgreementSchema. parse ({
... data,
id: crypto. randomUUID (),
status: "draft" ,
createdAt: new Date (),
});
this.store. set (agreement.id, agreement);
return agreement;
}
get (id : string ) : Agreement | undefined {
return this.store. get (id);
}
list (opts ?: { status ?: AgreementStatus ; agentName ?: string }) : Agreement [] {
let agreements = Array. from (this.store. values ());
if (opts) {
if (opts.status) {
agreements = agreements. filter ((a) => a.status === opts.status);
}
if (opts.agentName) {
const nameFilter = opts.agentName. toLowerCase ();
agreements = agreements. filter ((a) => a.agentName. toLowerCase (). includes (nameFilter));
}
}
return agreements;
}
update (id : string , patch : Partial < Omit < Agreement , "id" >>) : Agreement | undefined {
const existing = this.store. get (id);
if ( ! existing) return undefined ;
const updated = AgreementSchema. parse ({ ... existing, ... patch });
this.store. set (id, updated);
return updated;
}
sign (id : string ) : Agreement {
const existing = this.store. get (id);
if ( ! existing) throw new Error ( `Agreement ${ id } not found` );
if (existing.status === "signed" ) throw new Error ( `Agreement ${ id } is already signed` );
const updated = this. update (id, { status: "signed" , signedAt: new Date () });
return updated as Agreement ;
}
archive (id : string ) : Agreement | undefined {
return this. update (id, { status: "archived" });
}
findExpiring (withinDays : number ) : Agreement [] {
const now = Date. now ();
const limit = withinDays * 24 * 60 * 60 * 1000 ;
return Array. from (this.store. values ()). filter ((a) => {
if (a.status === "archived" || a.status === "expired" ) return false ;
if (a.expiresAt. getTime () <= now) return false ;
return a.expiresAt. getTime () - now <= limit;
});
}
}
Step 6: Build the memory service with REAA packages Now wire up the first three REAA packages. Create src/services/memory-service.ts that wraps AgentMemory from @reaatech/agent-memory, uses InMemoryMemoryStorage from @reaatech/agent-memory-storage, and exposes methods to store compliance facts and retrieve context.
import { AgentMemory, OpenAILLMProvider, MemoryType } from "@reaatech/agent-memory" ;
import type { Memory, ConversationTurn as PkgConversationTurn } from "@reaatech/agent-memory" ;
import { InMemoryMemoryStorage } from "@reaatech/agent-memory-storage" ;
import { config } from "../lib/config" ;
export class MemoryService {
private memory : AgentMemory ;
constructor (memory ?: AgentMemory ) {
if (memory) {
this.memory = memory;
} else {
const inMemory = new InMemoryMemoryStorage ();
this.memory = new AgentMemory ({
storage: inMemory,
embedding: { provider: "openai" , model: config.embeddingModel, apiKey: config.openaiApiKey },
extraction: {
llmProvider: new OpenAILLMProvider ({ apiKey: config.openaiApiKey, model: "gpt-4o-mini" }),
enabledTypes: [MemoryType.FACT, MemoryType.PREFERENCE],
batchSize: 5 ,
confidenceThreshold: 0.7 ,
},
});
}
}
async storeComplianceFact (agreementId : string , factDescription : string , type : MemoryType ) : Promise < void > {
const turn : PkgConversationTurn = { speaker: "agent" , content: `[${ agreementId }][${ type }] ${ factDescription }` , timestamp: new Date () };
await this.memory. extractAndStore ([turn]);
}
async retrieveComplianceContext (query : string , opts ?: { limit ?: number ; tenantId ?: string }) : Promise < Memory []> {
return this.memory. retrieve (query, { limit: opts?.limit ?? 5 , tenantId: opts?.tenantId ?? "default" });
}
async runMemoryMaintenance () : Promise < void > {
await this.memory. runMaintenance ();
}
async close () : Promise < void > {
await this.memory. close ();
}
}
Step 7: Build the compliance service Create src/services/compliance-service.ts — this is the core business logic. It defines five compliance rules and runs them against each agreement.
import type { Agreement, ComplianceFinding, ComplianceRule, ComplianceCheckResult } from "../lib/types" ;
import { AgreementService } from "./agreement-service" ;
import { MemoryService } from "./memory-service" ;
import { MemoryType } from "@reaatech/agent-memory" ;
const RULES : ComplianceRule [] = [
{
id: "disclosures-signed" ,
code: "REG-101" ,
description: "All required disclosures must be signed within 3 days of creation" ,
category: "disclosures" ,
severity: "critical" ,
validate (agreement : Agreement ) : ComplianceFinding {
Step 8: Build the RAG pipeline with ContextInjector Create src/services/compliance-rag.ts. This pipeline retrieves relevant memories, formats them via ContextInjector from @reaatech/agent-memory-retrieval, and calls the Vercel AI SDK to answer compliance questions.
import { generateText } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
import { ContextInjector } from "@reaatech/agent-memory-retrieval" ;
import type { Memory } from "@reaatech/agent-memory" ;
import { MemoryService } from "./memory-service" ;
import { AgreementService } from "./agreement-service" ;
import { ComplianceService } from "./compliance-service" ;
export class ComplianceRagPipeline {
private injector : ContextInjector ;
constructor (
private memory : MemoryService ,
private
Step 9: Build the evaluation service with gates Create src/services/eval-service.ts that loads evaluation datasets, validates samples, runs the RAG pipeline against them, and evaluates quality gates via the REAA evaluation packages.
import { DatasetLoader, DatasetValidator, loadEvalConfig } from "@reaatech/rag-eval-dataset" ;
import type { ValidationResult } from "@reaatech/rag-eval-dataset" ;
export { loadEvalConfig };
import { GateEngine, CIIntegration } from "@reaatech/rag-eval-gate" ;
import { EvaluationSampleSchema, type EvaluationSample, type GateResult, type EvalResults, type EvalSuiteConfig, type SampleEvalResult } from "@reaatech/rag-eval-core" ;
import { ComplianceService } from "./compliance-service" ;
import { ComplianceRagPipeline } from "./compliance-rag" ;
export class EvalService {
private loader = new DatasetLoader ();
Step 10: Wire up the Hono API Create src/api/services.ts to instantiate all services as singletons:
import { AgreementService } from "../services/agreement-service" ;
import { MemoryService } from "../services/memory-service" ;
import { ComplianceService } from "../services/compliance-service" ;
import { ComplianceRagPipeline } from "../services/compliance-rag" ;
import { EvalService } from "../services/eval-service" ;
import { config } from "../lib/config" ;
export const agreementService = new AgreementService ();
export const memoryService = new MemoryService ();
export const complianceService = new ComplianceService (agreementService, memoryService);
export const ragPipeline = new ComplianceRagPipeline (memoryService, config.modelName, agreementService, complianceService);
export const evalService = new EvalService (complianceService, ragPipeline); Create src/api/index.ts — the Hono app that mounts all route groups:
import { Hono } from "hono" ;
import { cors } from "hono/cors" ;
import { agreementRoutes } from "./routes/agreements" ;
import { complianceRoutes } from "./routes/compliance" ;
import { evalRoutes } from "./routes/eval" ;
export const app = new Hono (). basePath ( "/api" );
app. use ( "*" , cors ());
app. route ( "/agreements" , agreementRoutes);
app. route ( "/compliance" , complianceRoutes);
app. route ( "/eval" , evalRoutes); Create the three route files. Start with src/api/routes/agreements.ts:
import { Hono } from "hono" ;
import type { Context } from "hono" ;
import { agreementService } from "../services" ;
import { AgreementSchema, type AgreementStatus } from "../../lib/types" ;
export const agreementRoutes = new Hono ();
agreementRoutes. get ( "/" , (c : Context ) => {
const status = c.req. query ( "status" );
const agentName = c.req. query ( "agentName" );
const agreements = agreementService. list ({ status: status as AgreementStatus | undefined , agentName });
return c. json (agreements);
});
agreementRoutes. get ( "/expiring" , (c : Context ) => {
const withinDays = parseInt (c.req. query ( "withinDays" ) ?? "30" , 10 );
const agreements = agreementService. findExpiring (withinDays);
return c. json (agreements);
});
agreementRoutes. get ( "/:id" , (c : Context ) => {
const id = c.req. param ( "id" );
/* v8 ignore next */ if ( ! id) return c. json ({ error: "Not found" }, 404 );
const agreement = agreementService. get (id);
if ( ! agreement) return c. json ({ error: "Not found" }, 404 );
return c. json (agreement);
});
agreementRoutes. post ( "/" , async (c : Context ) => {
try {
const body : unknown = await c.req. json ();
const parsed = AgreementSchema. omit ({ id: true , status: true , createdAt: true }). parse (body);
const agreement = agreementService. create (parsed);
return c. json (agreement, 201 );
} catch {
return c. json ({ error: "Invalid request body" }, 400 );
}
});
agreementRoutes. patch ( "/:id" , async (c : Context ) => {
const id = c.req. param ( "id" );
/* v8 ignore next */ if ( ! id) return c. json ({ error: "Not found" }, 404 );
try {
const body : Record < string , unknown > = await c.req. json ();
const agreement = agreementService. update (id, body);
if ( ! agreement) return c. json ({ error: "Not found" }, 404 );
return c. json (agreement);
} catch {
return c. json ({ error: "Invalid request body" }, 400 );
}
});
agreementRoutes. delete ( "/:id" , (c : Context ) => {
const id = c.req. param ( "id" );
/* v8 ignore next */ if ( ! id) return c. json ({ error: "Not found" }, 404 );
const agreement = agreementService. archive (id);
if ( ! agreement) return c. json ({ error: "Not found" }, 404 );
return c. json ({ ok: true });
});
agreementRoutes. post ( "/:id/sign" , (c : Context ) => {
const id = c.req. param ( "id" );
/* v8 ignore next */ if ( ! id) return c. json ({ error: "Not found" }, 404 );
try {
const agreement = agreementService. sign (id);
return c. json (agreement);
} catch {
return c. json ({ error: "Not found" }, 404 );
}
}); Create src/api/routes/compliance.ts:
import { Hono } from "hono" ;
import type { Context } from "hono" ;
import { complianceService, ragPipeline } from "../services" ;
export const complianceRoutes = new Hono ();
complianceRoutes. post ( "/check/:id" , async (c : Context ) => {
const id = c.req. param ( "id" ) ?? "" ;
const result = await complianceService. checkAgreement (id);
return c. json (result);
});
complianceRoutes. post ( "/check-all" , async (c : Context ) => {
const results = await complianceService. checkAll ();
return c. json (results);
});
complianceRoutes. get ( "/summary" , async (c : Context ) => {
const summary = await complianceService. getSummary ();
return c. json (summary);
});
complianceRoutes. post ( "/query" , async (c : Context ) => {
const body : Record < string , unknown > = await c.req. json ();
const question = typeof body.question === "string" ? body.question : "" ;
const agreementId = typeof body.agreementId === "string" ? body.agreementId : undefined ;
const result = await ragPipeline. query (question, agreementId);
return c. json (result);
});
complianceRoutes. post ( "/report/:id" , async (c : Context ) => {
const id = c.req. param ( "id" ) ?? "" ;
const report = await ragPipeline. generateReport (id);
return c. json ({ report });
}); Create src/api/routes/eval.ts:
import { Hono } from "hono" ;
import type { Context } from "hono" ;
import type { EvaluationSample } from "@reaatech/rag-eval-core" ;
import { evalService } from "../services" ;
export const evalRoutes = new Hono ();
evalRoutes. post ( "/run" , async (c : Context ) => {
const body : Record < string , unknown > = await c.req. json ();
const datasetPath = typeof body.datasetPath === "string" && body.datasetPath ? body.datasetPath : "" ;
let samples : EvaluationSample [];
if ( ! datasetPath) {
const inline = body.inlineSamples;
samples = Array. isArray (inline) ? inline as EvaluationSample [] : [];
} else {
samples = await evalService. loadDataset (datasetPath);
}
const validationResult = evalService. validate (samples);
if ( ! validationResult.valid) {
return c. json ({ error: "Validation failed" , details: validationResult.errors }, 400 );
}
const { results, gateResult } = await evalService. evaluate (samples);
const report = evalService. formatReport (gateResult);
return c. json ({ results, gateResult, report });
});
evalRoutes. post ( "/validate" , async (c : Context ) => {
const body : Record < string , unknown > = await c.req. json ();
const samples = Array. isArray (body.samples) ? body.samples as EvaluationSample [] : [];
const result = evalService. validate (samples);
return c. json (result);
});
evalRoutes. get ( "/gates" , (c : Context ) => {
return c. json ({ gates: evalService. getGates () });
}); Expected output: The Hono API now exposes 15+ endpoints under /api/agreements, /api/compliance, and /api/eval.
Step 11: Mount Hono in Next.js and build the dashboard Create app/api/[[...route]]/route.ts — a catch-all Next.js route handler that proxies every HTTP method to the Hono app:
/* v8 ignore next 8 */
import { type NextRequest } from "next/server" ;
import { app } from "../../../src/api/index" ;
export async function GET (req : NextRequest ) { return app. fetch (req); }
export async function POST (req : NextRequest ) { return app. fetch (req); }
export async function PUT (req : NextRequest ) { return app. fetch (req); }
export async function PATCH (req : NextRequest ) { return app. fetch (req); }
export async function DELETE (req : NextRequest ) { return app. fetch (req); } Next, build the compliance dashboard in app/page.tsx. This server-rendered page fetches the compliance summary and expiring agreements at request time:
import { RagForm } from "./rag-form" ;
interface Summary {
total : number ;
passes : number ;
failures : number ;
warnings : number ;
avgScore : number ;
}
interface ExpiringAgreement {
id : string ;
propertyAddress : string ;
expiresAt : string ;
}
async function fetchJSON < T >(url : string ) : Promise
Create app/rag-form.tsx — a client component that posts RAG queries:
"use client" ;
import { useState } from "react" ;
export function RagForm () {
const [question, setQuestion] = useState ( "" );
const [answer, setAnswer] = useState ( "" );
const [sources, setSources] = useState < Array <{ id : string ; content : string }>>([]);
function handleSubmit (e : React . SyntheticEvent ) {
e. preventDefault ();
void fetch ( "/api/compliance/query" , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON. stringify ({ question }),
}). then ( async (res) => {
if ( ! res.ok) return ;
const data = await res. json () as { answer ?: string ; sources ?: Array <{ id : string ; content : string }> };
setAnswer (data.answer ?? "" );
setSources (data.sources ?? []);
});
}
return (
<section style ={{ border: "1px solid #ddd" , borderRadius: "8px" , padding: "1.5rem" }}>
<h2 style ={{ fontSize: "1.25rem" , marginBottom: "1rem" }}>RAG Query</h2>
<form onSubmit ={ handleSubmit } style ={{ display: "flex" , gap: "0.75rem" , flexDirection: "column" }}>
<input
value ={ question }
onChange ={( e ) => { setQuestion ( e . target . value ); }}
type = "text"
placeholder = "Ask a compliance question..."
required
style ={{ padding: "0.6rem" , border: "1px solid #ccc" , borderRadius: "4px" , fontSize: "1rem" }}
/>
<div style ={{ display: "flex" , gap: "0.75rem" , alignItems: "center" }}>
<button type = "submit" style ={{ padding: "0.6rem 1.2rem" , background: "#2563eb" , color: "#fff" , border: "none" , borderRadius: "4px" , fontSize: "1rem" , cursor: "pointer" }}>
Ask
</button>
</div>
</form>
{ answer && (
<div style ={{ marginTop: "1rem" }}>
<p style ={{ background: "#f5f5f5" , padding: "0.75rem" , borderRadius: "4px" }}>{ answer }</p>
{ sources .length > 0 && (
<details style ={{ marginTop: "0.5rem" }}>
<summary style ={{ cursor: "pointer" , color: "#555" , fontSize: "0.85rem" }}>Sources ({ sources .length})</summary>
<ul style ={{ fontSize: "0.8rem" , color: "#777" , paddingLeft: "1.2rem" }}>
{ sources . map (( s ) => (
<li key ={ s . id } style ={{ marginTop: "0.25rem" }}>{ s . content . slice ( 0 , 120 )}</li>
))}
</ul>
</details>
)}
</div>
)}
</section>
);
}
Step 12: Write tests with MSW Set up MSW in tests/setup.ts to mock OpenAI API calls:
import { beforeAll, afterEach, afterAll } from "vitest" ;
import { setupServer } from "msw/node" ;
import { http, HttpResponse } from "msw" ;
export const server = setupServer (
http. post ( "https://api.openai.com/v1/embeddings" , () =>
HttpResponse. json ({
data: [{ embedding: new Array ( 1536 ). fill ( 0.1 ) }],
model: "text-embedding-3-small" ,
usage: { prompt_tokens: 5 , total_tokens: 5 },
}),
),
http. post ( "https://api.openai.com/v1/chat/completions" , () =>
HttpResponse. json ({
id: "cmpl-test" ,
model: "gpt-5.2-mini" ,
choices: [
{
index: 0 ,
message: { role: "assistant" , content: "mocked compliance answer" },
finish_reason: "stop" ,
},
],
usage: { prompt_tokens: 20 , completion_tokens: 30 , total_tokens: 50 },
}),
),
);
beforeAll (() => {
server. listen ({ onUnhandledRequest: "error" });
});
afterEach (() => {
server. resetHandlers ();
});
afterAll (() => {
server. close ();
}); Write integration tests in tests/api/routes.test.ts that hit the Hono API directly:
import { vi, describe, it, expect, beforeAll } from "vitest" ;
class MockAgentMemory {
extractAndStore = vi. fn (). mockResolvedValue ([]);
retrieve = vi. fn (). mockResolvedValue ([]);
runMaintenance = vi. fn (). mockResolvedValue ( undefined );
close = vi. fn (). mockResolvedValue ( undefined );
}
vi. mock ( "@reaatech/agent-memory" , () => ({
AgentMemory: MockAgentMemory,
OpenAILLMProvider: vi. fn (),
MemoryType: { FACT: "fact" , PREFERENCE: "preference"
Step 13: Run the full test suite Run typecheck, lint, and tests:
pnpm typecheck && pnpm lint && pnpm test
pnpm typecheck exits 0 with no TypeScript errors
pnpm lint exits 0 with no ESLint violations
pnpm test runs vitest with coverage and exits 0, producing a vitest-report.json with passing results and coverage thresholds (lines, branches, functions, statements) all at or above 90%
Next steps
Add a PostgreSQL storage adapter — swap the in-memory storage with PostgresMemoryStorage from @reaatech/agent-memory-storage to persist compliance facts across restarts
Add more compliance rules — extend the RULES array with state-specific regulations (e.g., California’s TDS, New York’s agency disclosure forms)
Add a webhook notifier — fire Slack or email alerts when a compliance check returns a failure, so broker-owners can act immediately
Add real-time updates — use Server-Sent Events or WebSockets to push compliance check results to the dashboard without page refresh
Deploy with Langfuse — send traces to a production Langfuse instance to monitor LLM costs and latency per compliance query
const
unsignedRequired
=
agreement.disclosures.
filter
((d)
=>
d.required
&&
!
d.signedAt);
const recentUnsigned = unsignedRequired. filter (() => {
const age = Date. now () - agreement.createdAt. getTime ();
return age > 3 * 24 * 60 * 60 * 1000 ;
});
return {
ruleId: "disclosures-signed" ,
passed: recentUnsigned.length === 0 ,
message: recentUnsigned.length > 0
? String (recentUnsigned.length) + " required disclosure(s) not signed within 3 days"
: "All required disclosures signed within timeframe" ,
severity: "critical" ,
suggestedFix: recentUnsigned.length > 0 ? "Collect signatures on outstanding disclosures" : undefined ,
};
},
},
{
id: "buyer-signature" ,
code: "REG-102" ,
description: "Agreement must be signed by buyer" ,
category: "signatures" ,
severity: "critical" ,
validate (agreement : Agreement ) : ComplianceFinding {
return {
ruleId: "buyer-signature" ,
passed: agreement.status === "signed" ,
message: agreement.status === "signed" ? "Agreement is signed" : "Agreement has not been signed" ,
severity: "critical" ,
suggestedFix: agreement.status !== "signed" ? "Obtain buyer signature on the agreement" : undefined ,
};
},
},
{
id: "lead-paint" ,
code: "REG-103" ,
description: "Pre-1978 properties require lead-based paint disclosure" ,
category: "disclosures" ,
severity: "critical" ,
validate (agreement : Agreement ) : ComplianceFinding {
const isPre1978 = agreement.propertyAddress. toLowerCase (). includes ( "pre-1978" )
|| / \b (19[0-7][0-9] | 18[0-9] {2} ) \b / . test (agreement.propertyAddress);
const hasLeadDisclosure = agreement.disclosures. some ((d) => d.type === "lead-based-paint" );
const passed = ! isPre1978 || hasLeadDisclosure;
return {
ruleId: "lead-paint" ,
passed,
message: passed
? "Lead-based paint disclosure requirement satisfied"
: "Pre-1978 property requires lead-based paint disclosure" ,
severity: "critical" ,
suggestedFix: ! passed ? "Add lead-based paint disclosure to the agreement" : undefined ,
};
},
},
{
id: "form-current" ,
code: "REG-104" ,
description: "Disclosure forms must not be older than 365 days" ,
category: "disclosures" ,
severity: "warning" ,
validate (agreement : Agreement ) : ComplianceFinding {
const outdatedDisclosures = agreement.disclosures. filter ((d) => {
if ( ! d.signedAt) return false ;
const age = Date. now () - d.signedAt. getTime ();
return age > 365 * 24 * 60 * 60 * 1000 ;
});
return {
ruleId: "form-current" ,
passed: outdatedDisclosures.length === 0 ,
message: outdatedDisclosures.length > 0
? String (outdatedDisclosures.length) + " disclosure(s) older than 365 days"
: "All disclosure forms are current" ,
severity: "warning" ,
suggestedFix: outdatedDisclosures.length > 0 ? "Update outdated disclosure forms" : undefined ,
};
},
},
{
id: "no-expired" ,
code: "REG-105" ,
description: "Agreement status must not be expired" ,
category: "status" ,
severity: "info" ,
validate (agreement : Agreement ) : ComplianceFinding {
return {
ruleId: "no-expired" ,
passed: agreement.status !== "expired" ,
message: agreement.status === "expired" ? "Agreement has expired" : "Agreement is not expired" ,
severity: "info" ,
suggestedFix: agreement.status === "expired" ? "Renew or update the agreement" : undefined ,
};
},
},
];
export class ComplianceService {
constructor (
private agreementService : AgreementService ,
private memoryService : MemoryService ,
) {}
async checkAgreement (id : string ) : Promise < ComplianceCheckResult > {
const agreement = this.agreementService. get (id);
if ( ! agreement) {
return {
agreementId: id,
checkedAt: new Date (),
findings: [{
ruleId: "not-found" ,
passed: false ,
message: `Agreement ${ id } not found` ,
severity: "critical" ,
}],
overallStatus: "fail" ,
score: 0 ,
};
}
const findings = RULES. map ((rule) => rule. validate (agreement));
for ( const finding of findings) {
await this.memoryService. storeComplianceFact (
id,
`Rule ${ finding . ruleId }: ${ finding . message }` ,
MemoryType.FACT,
);
}
const passedCount = findings. filter ((f) => f.passed).length;
const score = Math. round ((passedCount / findings.length) * 100 );
const hasCriticalFailure = findings. some ((f) => ! f.passed && f.severity === "critical" );
const hasWarning = findings. some ((f) => ! f.passed && f.severity === "warning" );
let overallStatus : "pass" | "fail" | "warn" ;
if (hasCriticalFailure) {
overallStatus = "fail" ;
} else if (hasWarning) {
overallStatus = "warn" ;
} else {
overallStatus = "pass" ;
}
return {
agreementId: id,
checkedAt: new Date (),
findings,
overallStatus,
score,
};
}
async checkAll () : Promise < ComplianceCheckResult []> {
const agreements = this.agreementService. list ();
const results : ComplianceCheckResult [] = [];
for ( const agreement of agreements) {
if (agreement.status === "archived" ) continue ;
results. push ( await this. checkAgreement (agreement.id));
}
return results;
}
async getSummary () : Promise <{ total : number ; passes : number ; failures : number ; warnings : number ; avgScore : number }> {
const results = await this. checkAll ();
const total = results.length;
const passes = results. filter ((r) => r.overallStatus === "pass" ).length;
const failures = results. filter ((r) => r.overallStatus === "fail" ).length;
const warnings = results. filter ((r) => r.overallStatus === "warn" ).length;
const avgScore = total > 0 ? Math. round (results. reduce ((s, r) => s + r.score, 0 ) / total) : 100 ;
return { total, passes, failures, warnings, avgScore };
}
}
modelName
:
string
,
private agreementService : AgreementService ,
private complianceService : ComplianceService ,
) {
this.injector = new ContextInjector ( 100000 , 4 );
}
async query (question : string , agreementId ?: string ) : Promise <{
answer : string ;
sources : Memory [];
usage : { inputTokens : number ; outputTokens : number };
}> {
const memories = await this.memory. retrieveComplianceContext (question, {
limit: 10 ,
tenantId: "default" ,
});
const formattedContext = await this.injector. injectMemoriesIntoContext ([], memories, 4000 );
const scope = agreementId ? ` for agreement ${ agreementId }` : "" ;
const systemPrompt = `You are a real-estate compliance assistant. Answer questions about buyer-rep agreements, disclosures, and regulatory compliance${ scope }.` ;
const result = await generateText ({
model: openai (this.modelName),
system: systemPrompt,
prompt: formattedContext + "\n\nQuestion: " + question,
});
return {
answer: result.text,
sources: memories,
usage: {
inputTokens: (result as { usage ?: { inputTokens ?: number ; outputTokens ?: number } }).usage?.inputTokens ?? 0 ,
outputTokens: (result as { usage ?: { inputTokens ?: number ; outputTokens ?: number } }).usage?.outputTokens ?? 0 ,
},
};
}
async generateReport (agreementId : string ) : Promise < string > {
const agreement = this.agreementService. get (agreementId);
const complianceResult = await this.complianceService. checkAgreement (agreementId);
const memories = await this.memory. retrieveComplianceContext ( `compliance report for agreement ${ agreementId }` , {
limit: 10 ,
tenantId: "default" ,
});
const sectionFmt = (label : string , content : string ) => `--- ${ label } ---\n${ content }` ;
const agreementSection = agreement
? `ID: ${ agreement . id }\nProperty: ${ agreement . propertyAddress }\nBuyer: ${ agreement . buyerName }\nAgent: ${ agreement . agentName }\nStatus: ${ agreement . status }\nCreated: ${ agreement . createdAt . toISOString () }\nExpires: ${ agreement . expiresAt . toISOString () }`
: `Agreement ${ agreementId } not found` ;
const findingsSection = complianceResult.findings
. map ((f) => `[${ f . severity . toUpperCase () }] ${ f . ruleId }: ${ f . message } (${ f . passed ? "PASS" : "FAIL"})${ f . suggestedFix ? ` — Fix: ${ f . suggestedFix }` : ""}` )
. join ( "\n" );
const memoriesSection = memories
. map ((m) => `[${ m . type }] ${ m . content }` )
. join ( "\n" );
const context = [
sectionFmt ( "Agreement" , agreementSection),
sectionFmt ( "Compliance Check Results" , `Status: ${ complianceResult . overallStatus }\nScore: ${ String ( complianceResult . score ) }%\n\n${ findingsSection }` ),
sectionFmt ( "Related Memory Context" , memoriesSection || "No relevant memories found." ),
]. join ( "\n\n" );
const result = await generateText ({
model: openai (this.modelName),
system: "You are a real-estate compliance assistant. Generate a structured compliance status report based on the provided agreement data, compliance check results, and memory context." ,
prompt: `Generate a compliance status report for the following agreement:\n\n${ context }` ,
});
return result.text;
}
}
private validator = new DatasetValidator ();
private engine = new GateEngine ();
constructor (
private complianceService : ComplianceService ,
private ragPipeline : ComplianceRagPipeline ,
) {
this.engine. loadGates ([
{ name: "min-faithfulness" , type: "threshold" , metric: "avg_faithfulness" , operator: ">=" , threshold: 0.85 },
{ name: "min-relevance" , type: "threshold" , metric: "avg_relevance" , operator: ">=" , threshold: 0.80 },
]);
}
async loadDataset (path : string ) : Promise < EvaluationSample []> {
return this.loader. load (path);
}
validate (samples : EvaluationSample []) : ValidationResult {
try {
const validated = samples. map ((s) => EvaluationSampleSchema. parse (s));
return this.validator. validate (validated);
} catch {
return { valid: false , errors: [{ field: "validation" , message: "Schema validation failed" }], warnings: [] };
}
}
async evaluate (samples : EvaluationSample []) : Promise <{ results : EvalResults ; gateResult : GateResult }> {
const sampleResults : SampleEvalResult [] = [];
for ( let i = 0 ; i < samples.length; i ++ ) {
const sample = samples[i];
const ragResult = await this.ragPipeline. query (sample.query);
const faithfulness = this. computeOverlapScore (ragResult.answer, sample.ground_truth);
const relevance = this. computeOverlapScore (ragResult.answer, sample.context. join ( " " ));
sampleResults. push ({
sample_id: String (i),
sample,
faithfulness: { score: faithfulness, statements: [], supported_count: 0 , total_statements: 0 },
relevance: { score: relevance },
overall_score: (faithfulness + relevance) / 2 ,
evaluated_at: new Date (). toISOString (),
});
}
const metrics = {
overall_score: sampleResults. reduce ((s, r) => s + (r.overall_score ?? 0 ), 0 ) / sampleResults.length,
avg_faithfulness: sampleResults. reduce ((s, r) => s + (r.faithfulness?.score ?? 0 ), 0 ) / sampleResults.length,
avg_relevance: sampleResults. reduce ((s, r) => s + (r.relevance?.score ?? 0 ), 0 ) / sampleResults.length,
avg_context_precision: 0 ,
avg_context_recall: 0 ,
cost_per_sample: 0 ,
total_samples: samples.length,
};
const config : EvalSuiteConfig = {
metrics: [ "faithfulness" , "relevance" ],
gates: [
{ name: "min-faithfulness" , type: "threshold" , metric: "avg_faithfulness" , operator: ">=" , threshold: 0.85 },
{ name: "min-relevance" , type: "threshold" , metric: "avg_relevance" , operator: ">=" , threshold: 0.80 },
],
};
const evalResults : EvalResults = {
run_id: crypto. randomUUID (),
dataset: "inline" ,
config,
samples: sampleResults,
metrics,
total_cost: 0 ,
cost_breakdown: {
total: 0 ,
by_metric: {},
by_provider: {},
per_sample: [],
},
duration_ms: 0 ,
completed_at: new Date (). toISOString (),
};
const gateResult = this.engine. evaluate (evalResults);
return { results: evalResults, gateResult };
}
private computeOverlapScore (answer : string , reference : string ) : number {
/* v8 ignore next 2 */ if ( ! reference) return 0 ;
const answerWords = new Set (answer. toLowerCase (). split ( /\s + / ));
const refWords = reference. toLowerCase (). split ( /\s + / );
/* v8 ignore next 2 */ if (refWords.length === 0 ) return 0 ;
const matches = refWords. filter (w => answerWords. has (w)).length;
return Math. min (matches / refWords.length, 1 );
}
getGates () : Array <{ name : string ; type : string ; metric : string ; operator : string ; threshold : number }> {
return [
{ name: "min-faithfulness" , type: "threshold" , metric: "avg_faithfulness" , operator: ">=" , threshold: 0.85 },
{ name: "min-relevance" , type: "threshold" , metric: "avg_relevance" , operator: ">=" , threshold: 0.80 },
];
}
formatReport (gateResult : GateResult ) : string {
const ci = new CIIntegration ();
return ci. generateMarkdownReport (gateResult);
}
}
<
T
|
null
> {
const res = await fetch (url, { cache: "no-store" });
if ( ! res.ok) return null ;
return res. json () as T ;
}
export default async function Home () {
const summary = await fetchJSON < Summary >( "http://localhost:3000/api/compliance/summary" );
const expiring = await fetchJSON < ExpiringAgreement []>( "http://localhost:3000/api/agreements/expiring?withinDays=30" );
return (
<div style ={{ maxWidth: "900px" , margin: "0 auto" , padding: "2rem" , fontFamily: "system-ui, sans-serif" }}>
<h1 style ={{ fontSize: "1.75rem" , marginBottom: "0.5rem" }}>Compliance Dashboard</h1>
<p style ={{ color: "#666" , marginBottom: "2rem" }}>
Buyer-Rep Agreement Compliance Agent
</p>
<section style ={{ display: "flex" , gap: "1.5rem" , flexWrap: "wrap" , marginBottom: "2rem" }}>
< MetricCard label = "Total Agreements" value ={ summary ?. total ?? 0 } />
< MetricCard label = "Pass" value ={ summary ?. passes ?? 0 } color = "#16a34a" />
< MetricCard label = "Failures" value ={ summary ?. failures ?? 0 } color = "#dc2626" />
< MetricCard label = "Warnings" value ={ summary ?. warnings ?? 0 } color = "#f59e0b" />
< MetricCard label = "Avg Score" value ={ summary ? `${ String ( summary . avgScore ) }%` : 0 } />
</section>
{ expiring && expiring .length > 0 && (
<section style ={{ marginBottom: "2rem" }}>
<h2 style ={{ fontSize: "1.25rem" , marginBottom: "1rem" }}>Expiring Agreements (within 30 days)</h2>
<table style ={{ width: "100%" , borderCollapse: "collapse" }}>
<thead>
<tr style ={{ textAlign: "left" , borderBottom: "2px solid #ddd" }}>
<th style ={{ padding: "0.5rem" }}>ID</th>
<th style ={{ padding: "0.5rem" }}>Property</th>
<th style ={{ padding: "0.5rem" }}>Expires</th>
</tr>
</thead>
<tbody>
{ expiring . map (( a ) => (
<tr key ={ a . id } style ={{ borderBottom: "1px solid #eee" }}>
<td style ={{ padding: "0.5rem" , fontFamily: "monospace" , fontSize: "0.85rem" }}>{ a . id . slice ( 0 , 8 )}</td>
<td style ={{ padding: "0.5rem" }}>{ a . propertyAddress }</td>
<td style ={{ padding: "0.5rem" }}>{new Date ( a . expiresAt ). toLocaleDateString ()}</td>
</tr>
))}
</tbody>
</table>
</section>
)}
{(! expiring || expiring .length === 0 ) && (
<p style ={{ color: "#888" }}>No expiring agreements found.</p>
)}
< RagForm />
</div>
);
}
function MetricCard ({ label, value, color } : { label : string ; value : string | number ; color ?: string }) {
return (
<div style ={{ border: "1px solid #ddd" , borderRadius: "8px" , padding: "1rem 1.5rem" , minWidth: "140px" , textAlign: "center" }}>
<div style ={{ fontSize: "1.75rem" , fontWeight: "bold" , color: color ?? "#333" }}>{ value }</div>
<div style ={{ fontSize: "0.8rem" , color: "#666" , marginTop: "0.25rem" }}>{ label }</div>
</div>
);
}
},
}));
vi. mock ( "ai" , () => ({
generateText: vi. fn (). mockResolvedValue ({
text: "mocked compliance answer" ,
usage: { inputTokens: 10 , outputTokens: 5 },
}),
}));
vi. mock ( "@ai-sdk/openai" , () => ({ openai: vi. fn () }));
class MockContextInjector {
injectMemoriesIntoContext = vi. fn (). mockResolvedValue ( "<relevant_memories>mock context</relevant_memories>" );
}
vi. mock ( "@reaatech/agent-memory-retrieval" , () => ({
ContextInjector: MockContextInjector,
RetrievalStrategy: { SEMANTIC: "semantic" },
}));
vi. mock ( "@reaatech/rag-eval-dataset" , () => ({
DatasetLoader: vi. fn ( function () {
return {
load: vi. fn (). mockResolvedValue ([]),
};
}),
DatasetValidator: vi. fn ( function () {
return {
validate: vi. fn (). mockReturnValue ({ valid: true , errors: [], warnings: [] }),
};
}),
}));
vi. mock ( "@reaatech/rag-eval-gate" , () => ({
GateEngine: vi. fn ( function () {
return {
loadGates: vi. fn (),
evaluate: vi. fn (). mockReturnValue ({ passed: true , gates: [], results: {} }),
};
}),
CIIntegration: vi. fn ( function () {
return {
generateMarkdownReport: vi. fn (). mockReturnValue ( "# Compliance Report" ),
};
}),
}));
interface AgreementResponse {
id : string ;
status : string ;
buyerName ?: string ;
[key : string ] : unknown ;
}
interface ComplianceCheckResponse {
agreementId : string ;
findings : unknown [];
overallStatus : string ;
score : number ;
[key : string ] : unknown ;
}
interface QueryResponse {
answer : string ;
[key : string ] : unknown ;
}
interface ErrorResponse {
error : string ;
[key : string ] : unknown ;
}
describe ( "API routes" , () => {
let app : import ( "hono" ). Hono ;
beforeAll ( async () => {
const mod = await import ( "../../src/api/index" );
app = mod.app;
});
it ( "POST /api/agreements with valid body returns 201" , async () => {
const res = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "789 Pine Rd" ,
buyerName: "Test Buyer" ,
agentName: "Test Agent" ,
brokerName: "Test Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [],
}),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 201 );
const body = await res. json () as AgreementResponse ;
expect (body.id). toBeDefined ();
expect (body.status). toBe ( "draft" );
const getRes = await app. request ( `/api/agreements/${ body . id }` );
expect (getRes.status). toBe ( 200 );
const got = await getRes. json () as AgreementResponse ;
expect (got.id). toBe (body.id);
const patchRes = await app. request ( `/api/agreements/${ body . id }` , {
method: "PATCH" ,
body: JSON. stringify ({ buyerName: "Updated Buyer" }),
headers: { "Content-Type" : "application/json" },
});
expect (patchRes.status). toBe ( 200 );
const patched = await patchRes. json () as AgreementResponse ;
expect (patched.buyerName). toBe ( "Updated Buyer" );
const deleteRes = await app. request ( `/api/agreements/${ body . id }` , {
method: "DELETE" ,
});
expect (deleteRes.status). toBe ( 200 );
});
it ( "POST /api/compliance/check/:id returns findings" , async () => {
const createRes = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "123 Elm St" ,
buyerName: "Compliance Buyer" ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [
{
id: "d1" ,
type: "agency-disclosure" ,
title: "Agency Disclosure" ,
signedAt: new Date (). toISOString (),
required: true ,
},
],
}),
headers: { "Content-Type" : "application/json" },
});
const agreement = await createRes. json () as AgreementResponse ;
const checkRes = await app. request ( `/api/compliance/check/${ agreement . id }` , {
method: "POST" ,
});
expect (checkRes.status). toBe ( 200 );
const checkBody = await checkRes. json () as ComplianceCheckResponse ;
expect (checkBody.agreementId). toBe (agreement.id);
expect (Array. isArray (checkBody.findings)). toBe ( true );
expect ( typeof checkBody.overallStatus). toBe ( "string" );
expect ( typeof checkBody.score). toBe ( "number" );
});
it ( "POST /api/compliance/query returns answer" , async () => {
const res = await app. request ( "/api/compliance/query" , {
method: "POST" ,
body: JSON. stringify ({
question: "disclosure rules" ,
agreementId: "test-id" ,
}),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as QueryResponse ;
expect ( typeof body.answer). toBe ( "string" );
expect (body.answer.length). toBeGreaterThan ( 0 );
});
it ( "POST /api/agreements with bad JSON returns 400" , async () => {
const res = await app. request ( "/api/agreements" , {
method: "POST" ,
body: "not-json" ,
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 400 );
const body = await res. json () as ErrorResponse ;
expect (body.error). toBeDefined ();
});
it ( "GET /api/agreements/non-existent returns 404" , async () => {
const res = await app. request ( "/api/agreements/non-existent-id" );
expect (res.status). toBe ( 404 );
});
it ( "GET /api/agreements returns empty array when none exist" , async () => {
const res = await app. request ( "/api/agreements" );
expect (res.status). toBe ( 200 );
const body = await res. json () as unknown [];
expect (Array. isArray (body)). toBe ( true );
});
it ( "GET /api/agreements/expiring?withinDays=30 returns expiring agreements" , async () => {
const createRes = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "123 Expiring Ln" ,
buyerName: "Expiring Test" ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 15 * 86400000 ). toISOString (),
disclosures: [],
}),
headers: { "Content-Type" : "application/json" },
});
expect (createRes.status). toBe ( 201 );
const created = await createRes. json () as AgreementResponse ;
const res = await app. request ( "/api/agreements/expiring?withinDays=30" );
expect (res.status). toBe ( 200 );
const body = await res. json () as AgreementResponse [];
expect (Array. isArray (body)). toBe ( true );
const found = body. find ((a : AgreementResponse ) => a.id === created.id);
expect (found). toBeDefined ();
});
it ( "POST /api/agreements/:id/sign signs an agreement" , async () => {
const createRes = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "456 Sign Ln" ,
buyerName: "Sign Test" ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [],
}),
headers: { "Content-Type" : "application/json" },
});
expect (createRes.status). toBe ( 201 );
const created = await createRes. json () as AgreementResponse ;
const signRes = await app. request ( `/api/agreements/${ created . id }/sign` , {
method: "POST" ,
});
expect (signRes.status). toBe ( 200 );
const signed = await signRes. json () as AgreementResponse ;
expect (signed.status). toBe ( "signed" );
});
it ( "POST /api/agreements with disclosures creates agreement with parsed disclosures" , async () => {
const res = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "789 Disclosure Ave" ,
buyerName: "Disc Buyer" ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [
{
id: "disc-1" ,
type: "agency-disclosure" ,
title: "Agency Disclosure" ,
signedAt: new Date (). toISOString (),
required: true ,
},
],
}),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 201 );
const body = await res. json () as AgreementResponse ;
expect (body.id). toBeDefined ();
expect (body.status). toBe ( "draft" );
});
it ( "PATCH /api/agreements/:id with invalid body returns 400" , async () => {
const createRes = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "999 Error Blvd" ,
buyerName: "Error Test" ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [],
}),
headers: { "Content-Type" : "application/json" },
});
expect (createRes.status). toBe ( 201 );
const created = await createRes. json () as AgreementResponse ;
const patchRes = await app. request ( `/api/agreements/${ created . id }` , {
method: "PATCH" ,
body: "not-json" ,
headers: { "Content-Type" : "application/json" },
});
expect (patchRes.status). toBe ( 400 );
const errBody = await patchRes. json () as ErrorResponse ;
expect (errBody.error). toBeDefined ();
});
it ( "POST /api/compliance/check-all returns array of results" , async () => {
for ( let i = 0 ; i < 2 ; i ++ ) {
await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: `${ String ( 100 + i ) } CheckAll St` ,
buyerName: `CheckAll ${ String ( i ) }` ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [],
}),
headers: { "Content-Type" : "application/json" },
});
}
const res = await app. request ( "/api/compliance/check-all" , {
method: "POST" ,
});
expect (res.status). toBe ( 200 );
const body = await res. json () as ComplianceCheckResponse [];
expect (Array. isArray (body)). toBe ( true );
expect (body.length). toBeGreaterThanOrEqual ( 2 );
});
it ( "GET /api/compliance/summary returns summary object" , async () => {
const res = await app. request ( "/api/compliance/summary" , {
method: "GET" ,
});
expect (res.status). toBe ( 200 );
const body = await res. json () as { total : number ; passes : number ; failures : number ; warnings : number ; avgScore : number };
expect ( typeof body.total). toBe ( "number" );
expect ( typeof body.passes). toBe ( "number" );
expect ( typeof body.failures). toBe ( "number" );
expect ( typeof body.warnings). toBe ( "number" );
expect ( typeof body.avgScore). toBe ( "number" );
});
it ( "POST /api/compliance/report/:id returns report string" , async () => {
const createRes = await app. request ( "/api/agreements" , {
method: "POST" ,
body: JSON. stringify ({
propertyAddress: "111 Report Rd" ,
buyerName: "Report Test" ,
agentName: "Agent" ,
brokerName: "Broker" ,
expiresAt: new Date (Date. now () + 30 * 86400000 ). toISOString (),
disclosures: [],
}),
headers: { "Content-Type" : "application/json" },
});
expect (createRes.status). toBe ( 201 );
const created = await createRes. json () as AgreementResponse ;
const reportRes = await app. request ( `/api/compliance/report/${ created . id }` , {
method: "POST" ,
});
expect (reportRes.status). toBe ( 200 );
const reportBody = await reportRes. json () as { report : string };
expect ( typeof reportBody.report). toBe ( "string" );
expect (reportBody.report.length). toBeGreaterThan ( 0 );
});
it ( "POST /api/eval/validate with empty samples returns valid" , async () => {
const res = await app. request ( "/api/eval/validate" , {
method: "POST" ,
body: JSON. stringify ({ samples: [] }),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as { valid : boolean };
expect (body). toBeDefined ();
});
it ( "GET /api/eval/gates returns gates object" , async () => {
const res = await app. request ( "/api/eval/gates" );
expect (res.status). toBe ( 200 );
const body = await res. json () as { gates : unknown [] };
expect (body). toBeDefined ();
expect (Array. isArray (body.gates)). toBe ( true );
});
it ( "POST /api/eval/run with datasetPath handles request" , async () => {
const res = await app. request ( "/api/eval/run" , {
method: "POST" ,
body: JSON. stringify ({ datasetPath: "/tmp/test-dataset.json" }),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as { results : unknown ; gateResult : unknown ; report : string };
expect (body.results). toBeDefined ();
expect (body.gateResult). toBeDefined ();
expect ( typeof body.report). toBe ( "string" );
});
it ( "POST /api/compliance/query without optional agreementId" , async () => {
const res = await app. request ( "/api/compliance/query" , {
method: "POST" ,
body: JSON. stringify ({ question: "test question" }),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as { answer : string ; sources : unknown [] };
expect (body.answer). toBeTruthy ();
});
it ( "POST /api/compliance/query with non-string question defaults to empty" , async () => {
const res = await app. request ( "/api/compliance/query" , {
method: "POST" ,
body: JSON. stringify ({ question: 123 }),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as { answer : string };
expect (body.answer). toBeTruthy ();
});
it ( "POST /api/eval/validate with no samples array uses empty default" , async () => {
const res = await app. request ( "/api/eval/validate" , {
method: "POST" ,
body: JSON. stringify ({}),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as { valid : boolean };
expect (body). toBeDefined ();
});
it ( "DELETE non-existent agreement returns 404" , async () => {
const res = await app. request ( "/api/agreements/non-existent-id" , { method: "DELETE" });
expect (res.status). toBe ( 404 );
});
it ( "POST sign non-existent agreement returns 404" , async () => {
const res = await app. request ( "/api/agreements/non-existent-id/sign" , { method: "POST" });
expect (res.status). toBe ( 404 );
});
it ( "POST /api/eval/run without datasetPath handles request" , async () => {
const res = await app. request ( "/api/eval/run" , {
method: "POST" ,
body: JSON. stringify ({}),
headers: { "Content-Type" : "application/json" },
});
expect (res.status). toBe ( 200 );
const body = await res. json () as Record < string , unknown >;
expect (body). toBeDefined ();
});
});