feat: Zod AI validation, AIProvenance governance, fix tests (154 green)
- Add AIProvenance + AIResponse<T> to IAIService — all 11 methods now return structured provenance (provider, model, source, fallbackUsed, validationPassed) instead of bare ItemResponse<T> - Add schemas.ts with Zod schemas for all 8 AI response types; validateAIResponse() utility returns null on failure, never throws - Rewrite OpenRouterAIService: every method validates AI JSON against its Zod schema; failed validation triggers MockAIService fallback with fallbackUsed:true — no invalid data can reach the UI - Fix MockAIService.generateFollowUpQuestions: replace broken mockParseNeed(JSON.stringify(criteria)) with direct ParsedNeedCriteria field inspection; returns max 3 prioritised FollowUpQuestion objects - Add provenance: mockProvenance() to all MockAIService responses - Improve decisionBriefPrompt: structured JSON schema example, confidence vocabulary, availability disclaimer - Improve matchExplanationPrompt: score-tier vocabulary, isFutureSignal flag forbids confirmed-availability language for future signals - Add 102 new tests: mustHaveScorer (16), softFactorEnrichment (38), aiSchemas (52) — 154 total, all passing; 0 TypeScript errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,52 @@
|
||||
import type { ItemResponse } from '../types'
|
||||
import type { CreateNeedInput } from '../../domain/need'
|
||||
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../../domain/needBuilder'
|
||||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||||
|
||||
// ── AI Provenance ─────────────────────────────────────────────────────────────
|
||||
// Attached to every AI response so the UI can always trace where data came from.
|
||||
|
||||
export interface AIProvenance {
|
||||
/** Which AI provider produced this response */
|
||||
provider: 'openrouter' | 'mock'
|
||||
/** Exact model ID (e.g. 'anthropic/claude-3-5-haiku') or 'mock' */
|
||||
model: string
|
||||
/** ISO-8601 timestamp of generation */
|
||||
generatedAt: string
|
||||
/** Prompt version string used to generate this response */
|
||||
promptVersion: string
|
||||
/** Whether the response is AI-only, mock-only, or a hybrid merge */
|
||||
source: 'ai' | 'mock' | 'hybrid'
|
||||
/** True when the original AI call failed and mock was substituted */
|
||||
fallbackUsed: boolean
|
||||
/** True when the AI response passed Zod schema validation */
|
||||
validationPassed: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* All AI service methods return AIResponse<T> instead of ItemResponse<T>.
|
||||
* `data` is backward-compatible — existing call sites using `result.data.xxx`
|
||||
* continue to work unchanged.
|
||||
*/
|
||||
export type AIResponse<T> = {
|
||||
data: T
|
||||
provenance: AIProvenance
|
||||
}
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function mockProvenance(overrides?: Partial<AIProvenance>): AIProvenance {
|
||||
return {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
generatedAt: new Date().toISOString(),
|
||||
promptVersion: 'mock',
|
||||
source: 'mock',
|
||||
fallbackUsed: false,
|
||||
validationPassed: true,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Response Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DecisionBrief {
|
||||
@@ -49,8 +93,6 @@ export interface ParsedListingData {
|
||||
fitOut?: string
|
||||
}
|
||||
|
||||
// ── New Response Types ────────────────────────────────────────────────────────
|
||||
|
||||
export interface MatchExplanation {
|
||||
headline: string
|
||||
summary: string
|
||||
@@ -124,29 +166,29 @@ export interface AIServiceProvider {
|
||||
|
||||
export interface IAIService {
|
||||
// Need parsing (F008)
|
||||
parseNeed(input: string): Promise<ItemResponse<ParseNeedResult>>
|
||||
generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise<ItemResponse<FollowUpQuestion[]>>
|
||||
parseNeed(input: string): Promise<AIResponse<ParseNeedResult>>
|
||||
generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise<AIResponse<FollowUpQuestion[]>>
|
||||
|
||||
// Match explainability (F004)
|
||||
generateMatchExplanation(input: MatchExplanationInput): Promise<ItemResponse<MatchExplanation>>
|
||||
summarizeTradeOffs(tradeoffs: TradeOffInput[]): Promise<ItemResponse<TradeOffSummary>>
|
||||
generateMatchExplanation(input: MatchExplanationInput): Promise<AIResponse<MatchExplanation>>
|
||||
summarizeTradeOffs(tradeoffs: TradeOffInput[]): Promise<AIResponse<TradeOffSummary>>
|
||||
|
||||
// Compare (F014)
|
||||
summarizeComparison(items: UnifiedMatchResult[]): Promise<ItemResponse<ComparisonSummary>>
|
||||
summarizeComparison(items: UnifiedMatchResult[]): Promise<AIResponse<ComparisonSummary>>
|
||||
|
||||
// Shortlist decision brief
|
||||
generateDecisionBrief(shortlistId: string): Promise<ItemResponse<DecisionBrief>>
|
||||
generateDecisionBrief(shortlistId: string): Promise<AIResponse<DecisionBrief>>
|
||||
|
||||
// Data quality
|
||||
generateDataQualitySummary(propertyId: string, quality: DataQualityInput): Promise<ItemResponse<DataQualitySummary>>
|
||||
generateDataQualitySummary(propertyId: string, quality: DataQualityInput): Promise<AIResponse<DataQualitySummary>>
|
||||
|
||||
// Market signal classification (OPERATIONS)
|
||||
classifyMarketSignal(signalText: string): Promise<ItemResponse<MarketSignalClassification>>
|
||||
classifyMarketSignal(signalText: string): Promise<AIResponse<MarketSignalClassification>>
|
||||
|
||||
// Offer email (supply side)
|
||||
generateOfferEmail(payload: OfferEmailPayload): Promise<ItemResponse<{ subject: string; body: string }>>
|
||||
generateOfferEmail(payload: OfferEmailPayload): Promise<AIResponse<{ subject: string; body: string }>>
|
||||
|
||||
// Legacy methods
|
||||
extractCriteria(input: string): Promise<ItemResponse<CriteriaExtractionResult>>
|
||||
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<ItemResponse<string[]>>
|
||||
extractCriteria(input: string): Promise<AIResponse<CriteriaExtractionResult>>
|
||||
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<AIResponse<string[]>>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user