import { isAccepted, parseJudgeResult } from "./judge.ts"; import { redactPromptSensitiveText } from "./redaction.ts"; import { isDegenerateCandidateSummary } from "./summary.ts"; import type { CompleteTextFn, JudgeResult } from "./types.ts"; export type SlipstreamSourceEvidence = { sourceMessageExcerpts?: string[]; filesModified?: string[]; unresolvedErrors?: string[]; userDecisions?: string[]; constraints?: string[]; openLoops?: string[]; recentVerification?: string[]; latestUpdates?: string[]; retainedTailUpdates?: string[]; latestExchangeState?: string[]; terminalFinalAnswerEvidence?: string[]; staleSignals?: string[]; criticalLiterals?: string[]; }; export type ValidateOnlyInput = { candidate: string; sourceEvidence: SlipstreamSourceEvidence; continuation?: string | string[]; completeText: CompleteTextFn; config?: { judgeThreshold?: number; repairAttempts?: number; repairEnabled?: boolean; }; signal?: AbortSignal; }; export type ValidationResult = { summary: string; accepted: boolean; repaired: boolean; score: number; missing: string[]; contradictions: string[]; diagnosis: string; repairCount: number; judge: JudgeResult; }; function list(lines: readonly string[] | undefined): string { if (!lines?.length) return "- None"; return lines.map((line) => `- ${line}`).join("\n"); } function renderContinuation( continuation: ValidateOnlyInput["continuation"], ): string { if (continuation === undefined) return "- None"; if (Array.isArray(continuation)) return list(continuation); return continuation.trim() ? continuation : "- None"; } function renderEvidence(evidence: SlipstreamSourceEvidence): string { return `Source message excerpts:\n${list(evidence.sourceMessageExcerpts)}\n\nFiles modified:\n${list(evidence.filesModified)}\n\nUnresolved errors:\n${list(evidence.unresolvedErrors)}\n\nUser decisions:\n${list(evidence.userDecisions)}\n\nConstraints:\n${list(evidence.constraints)}\n\nOpen loops:\n${list(evidence.openLoops)}\n\nRecent verification:\n${list(evidence.recentVerification)}\n\nLatest updates:\n${list(evidence.latestUpdates)}\n\nRetained-tail updates:\n${list(evidence.retainedTailUpdates)}\n\nLatest exchange state:\n${list(evidence.latestExchangeState)}\n\nTerminal final-answer evidence:\n${list(evidence.terminalFinalAnswerEvidence)}\n\nStale or superseded signals:\n${list(evidence.staleSignals)}\n\nCritical literals:\n${list(evidence.criticalLiterals)}`; } function buildExternalJudgePrompt( candidate: string, input: ValidateOnlyInput, ): string { return redactPromptSensitiveText(`You are the Slipstream artifact-free integration API judge. Judge the candidate summary against only the caller-provided source evidence and continuation. This API is artifact-free: do not assume hidden local files, stats, widgets, pending state, or ctx.compact side effects exist.\n\nReturn only JSON with this shape:\n{\n "score": 0-10,\n "decision": "accept" | "reject",\n "missing": ["..."],\n "contradictions": ["..."],\n "diagnosis": "..."\n}\n\nReject if the candidate omits current constraints, unresolved errors, user decisions, or next-action information needed for safe continuation. Accept safe score-8 handoffs with advisory warnings when current state and next action are clear. Do not let advisory missing[] items veto acceptance, but reject critical contradictions. Live/manual/browser/API/integration/smoke verification outranks unit tests when it exercises the changed surface; passing unit/lint/typecheck evidence should be terse, while actionable failing checks need command/error and evidenced attribution. Hard-reject secret-shaped values, contradictions involving auth/cert/key/deletion/deploy state, wrong current dirty-state claims, terminal-vs-pending contradictions, and stale current-state claims that can mislead the next action.\n\nSource evidence:\n${renderEvidence(input.sourceEvidence)}\n\nContinuation evidence:\n${renderContinuation(input.continuation)}\n\nCandidate summary:\n\n${candidate}\n`); } function buildExternalRepairPrompt( candidate: string, judge: JudgeResult, input: ValidateOnlyInput, ): string { return redactPromptSensitiveText(`Rewrite the full candidate summary for the Slipstream artifact-free integration API. Preserve true useful content from the candidate, fix the judge findings, and ground the summary only in caller-provided source evidence and continuation. Do not mention hidden artifacts, stats, widgets, pending state, or ctx.compact side effects. Use the same paired policy as main Slipstream: make the current state and next action clear enough that safe score-8 handoffs can be accepted with advisory warnings, while fixing the causes of hard rejects. Start repaired summaries with "Continuation card:" as the first line; if your draft starts with ## Goal or any deterministic capsule, rewrite it before returning. Keep the model-facing handoff under roughly 100-150 lines unless extra length is required for safety. Do not include both a verbatim terminal answer and a synthesized restatement of the same facts; digest long terminal answers into 1-3 bullets and keep exact text only as recoverable evidence. Use one active-file list backed by current evidence when available; historical file manifests belong only behind recovery pointers. flatten copied markdown headings, raw tool markers, and log headings before including their content; copied output must not create repeated real sections in the summary. Live/manual/browser/API/integration/smoke verification should lead when it exercises the changed surface. Keep passing unit/lint/typecheck evidence terse. Preserve actionable failing checks with command/error and evidenced attribution. Normalize ## Verification / Evidence into a compact table when multiple checks/signals exist:\n\n| Check | Status | Freshness | Relevance |\n|---|---|---|---|\n| | passed/failed/not run/unknown | latest/superseded/pre-existing/after edit | why it matters |\n\nRedact secret-shaped values; never print API keys, tokens, passwords, private keys, certificates, or bearer values. For auth/cert/key/deletion/deploy state, wrong current dirty-state claims, terminal-vs-pending contradictions, and stale current-state claims, do not smooth over contradictions: preserve exact current evidence, mark unknown/unverified, or make recheck the next action. Judge diagnosis: ${judge.diagnosis} Missing facts: ${list(judge.missing)} Contradictions: ${list(judge.contradictions)} Source evidence: ${renderEvidence(input.sourceEvidence)} Continuation evidence: ${renderContinuation(input.continuation)} Previous candidate summary: ${candidate} `); } function clampRepairAttempts(value: number | undefined): number { if (value === undefined) return 1; if (!Number.isFinite(value)) return 1; return Math.max(0, Math.min(3, Math.floor(value))); } export async function slipstreamStyleValidateAndRepair( input: ValidateOnlyInput, ): Promise { const threshold = input.config?.judgeThreshold ?? 7; const repairEnabled = input.config?.repairEnabled !== false; const repairAttempts = repairEnabled ? clampRepairAttempts(input.config?.repairAttempts) : 0; let summary = input.candidate; let judge = parseJudgeResult( await input.completeText( buildExternalJudgePrompt(summary, input), input.signal, ), ); let repaired = false; let repairCount = 0; let bestAcceptedSummary = isAccepted(judge, threshold, summary) ? summary : null; let bestAcceptedJudge = isAccepted(judge, threshold, summary) ? judge : null; for ( let attempt = 0; attempt < repairAttempts && !isAccepted(judge, threshold, summary); attempt += 1 ) { const repairedSummary = await input.completeText( buildExternalRepairPrompt(summary, judge, input), input.signal, ); repaired = true; if (isDegenerateCandidateSummary(repairedSummary)) continue; repairCount += 1; summary = repairedSummary; judge = parseJudgeResult( await input.completeText( buildExternalJudgePrompt(summary, input), input.signal, ), ); if ( isAccepted(judge, threshold, summary) && (bestAcceptedJudge === null || judge.score > bestAcceptedJudge.score) ) { bestAcceptedSummary = summary; bestAcceptedJudge = judge; } } if (bestAcceptedSummary !== null && bestAcceptedJudge !== null) { summary = bestAcceptedSummary; judge = bestAcceptedJudge; } return { summary, accepted: isAccepted(judge, threshold, summary), repaired, score: judge.score, missing: judge.missing, contradictions: judge.contradictions, diagnosis: judge.diagnosis, repairCount, judge, }; }