// Analyze pass: extract requirements from prose with traceability hints. import "server-only"; import { defaultGateway, chatJSON, type Message } from "../gateway"; import { loadPrompt } from "../prompts"; import type { DetectedRequirement } from "../../db/repo"; const reqsJsonSchema = { type: "object", additionalProperties: false, required: ["requirements"], properties: { requirements: { type: "array", items: { type: "object", additionalProperties: false, required: ["tag", "text"], properties: { tag: { type: "string", minLength: 1 }, text: { type: "string", minLength: 1 }, tracedToLabels: { type: "array", items: { type: "string" } }, /// Optional concept the requirement is conceptually "about." Lets the /// requirement render a back-link to the term in TermDetail even when /// the term has no formalized block yet. linkedTermLabel: { type: "string" }, }, }, }, }, } as const; export interface RequirementsResult { reqs: DetectedRequirement[]; inputTokens: number; outputTokens: number; provider: string; model: string; } export async function analyzeRequirements( documentText: string, terms: Array<{ id: string; label: string; linkedBlockId: string | null }> ): Promise { const prompt = loadPrompt("socrates/analyze-requirements.md"); const gateway = defaultGateway(); const userPayload = `Document:\n\n${documentText}\n\n---\n\n` + `Available term labels:\n${JSON.stringify(terms.map(t => t.label))}`; const messages: Message[] = [ { role: "system", content: prompt }, { role: "user", content: userPayload }, ]; const { value, result } = await chatJSON<{ requirements?: Array<{ tag?: string; text?: string; tracedToLabels?: string[]; linkedTermLabel?: string; }>; }>(gateway, messages, { temperature: 0.2, maxTokens: 1500, jsonSchema: { name: "requirements", schema: reqsJsonSchema as Record }, jsonObjectMode: true, maxRepairs: 2, }); // Map term labels → block ids via the term-to-block links + label → termId. const labelToBlock = new Map(); const labelToTermId = new Map(); for (const t of terms) { if (t.linkedBlockId) labelToBlock.set(t.label.toLowerCase(), t.linkedBlockId); labelToTermId.set(t.label.toLowerCase(), t.id); } const reqs: DetectedRequirement[] = []; for (const r of value?.requirements ?? []) { const tag = (r.tag ?? "").trim(); const text = (r.text ?? "").trim(); if (!tag || !text) continue; const blockIds = (r.tracedToLabels ?? []) .map(l => labelToBlock.get(l.toLowerCase())) .filter((id): id is string => Boolean(id)); const linkedTermId = r.linkedTermLabel ? labelToTermId.get(r.linkedTermLabel.toLowerCase()) : undefined; reqs.push({ tag, text, tracedToIds: blockIds, unsupported: blockIds.length === 0, linkedTermId: linkedTermId ?? null, }); } return { reqs, inputTokens: result.inputTokens, outputTokens: result.outputTokens, provider: gateway.provider, model: gateway.model, }; }