|
| 1 | +/** |
| 2 | + * Deposit-mode Validation agent — Validation phase (V48 Gate 2/3). |
| 3 | + * |
| 4 | + * The deposit lens of the SynthesizeAssetPacks Validation phase: validate the |
| 5 | + * synthesized, measured-patch AssetPacks (implementation:options / |
| 6 | + * implementation:assetPacks) before Finish uploads them for depositor review. |
| 7 | + * The read lens validates fits artifacts; the deposit lens validates the |
| 8 | + * QUALITY of the synthesized supply — measurement honesty, distinctness, |
| 9 | + * source-safety, obfuscation/exclusion compliance, patch coherence, and |
| 10 | + * repository coverage — and drives the iterate-vs-complete decision. |
| 11 | + * |
| 12 | + * Source-safety (hard constraint): validation describes quality, never raw |
| 13 | + * source. It reasons only over the source-safe AssetPack descriptors (title, |
| 14 | + * summary, coveredSourcePaths, measurements, confidence, and the source-safe |
| 15 | + * patch descriptor of fileChanges path+op + patchSummary), the inventory paths, |
| 16 | + * the obfuscation guidance, and the protected-IP exclusions — never raw code. |
| 17 | + * |
| 18 | + * Runs on the formal PTRR machinery (factoryAgentWithPTRR → Plan/Try/Refine/ |
| 19 | + * Retry) so every call renders in the SDIVF telemetry. The agent's qualitative |
| 20 | + * findings are merged with deterministic AssetPack smoke/sanity checks, and the |
| 21 | + * resulting issues are stored under validation/implementation:issues — the exact |
| 22 | + * store the AssetPack ReadyToFinish gate reads to decide finish vs. review. |
| 23 | + */ |
| 24 | + |
| 25 | +import { factoryAgentWithPTRR } from '@bitcode/agent-generics'; |
| 26 | +import { Prompt } from '@bitcode/prompts/prompt'; |
| 27 | +import type { PromptPart } from '@bitcode/prompts/parts/PromptPart'; |
| 28 | +import { z } from 'zod'; |
| 29 | +import { DEPOSIT_MEASUREMENT_CATALOG } from '../../asset-packs-synthesis'; |
| 30 | + |
| 31 | +const part = (content: string): PromptPart => content as PromptPart; |
| 32 | + |
| 33 | +const DepositValidationInputSchema = z.object({ |
| 34 | + assetPacks: z.any().optional(), |
| 35 | + inventory: z.any().optional(), |
| 36 | + obfuscationGuidance: z.any().optional(), |
| 37 | + protectedIpExclusions: z.any().optional(), |
| 38 | +}); |
| 39 | + |
| 40 | +const DepositValidationOutputSchema = z.object({ |
| 41 | + issues: z.array(z.string()), |
| 42 | + qualityScore: z.number().min(0).max(1), |
| 43 | + coverageGaps: z.array(z.string()), |
| 44 | + recommendation: z.enum(['complete', 'iterate']), |
| 45 | +}); |
| 46 | + |
| 47 | +export type DepositValidationResult = z.infer<typeof DepositValidationOutputSchema>; |
| 48 | + |
| 49 | +const IDENTITY = part( |
| 50 | + 'You are the SynthesizeAssetPacks Validation agent in DEPOSIT mode. You validate ' + |
| 51 | + 'the QUALITY of the synthesized, measured-patch AssetPacks the depositor will ' + |
| 52 | + 'review — never the raw source. You reason only over the source-safe AssetPack ' + |
| 53 | + 'descriptors, the repository inventory paths, the obfuscation guidance, and the ' + |
| 54 | + 'protected-IP exclusions. You describe quality and never quote, reconstruct, or ' + |
| 55 | + 'expose raw source, code, secrets, or file contents. Your verdict drives the ' + |
| 56 | + 'iterate-vs-complete decision for the deposit supply.', |
| 57 | +); |
| 58 | + |
| 59 | +const REQUIREMENTS = part( |
| 60 | + [ |
| 61 | + 'Validate the synthesized deposit AssetPacks against these checks and report ' + |
| 62 | + 'every concrete problem as a short, source-safe issue string:', |
| 63 | + '- Quality threshold: each pack carries honest 0..1 measurements (an object whose ' + |
| 64 | + 'values are in [0,1]) over the expected measurement kinds (' + |
| 65 | + DEPOSIT_MEASUREMENT_CATALOG.map((spec) => spec.measurementKind).join(', ') + |
| 66 | + ') with a confidence in [0,1] that is reasonable for the evidence; flag inflated, ' + |
| 67 | + 'out-of-range, or unjustified measurements/confidence.', |
| 68 | + '- Distinctness: the packs are genuinely distinct, complementary knowledge slices, ' + |
| 69 | + 'not duplicative or near-identical; flag overlap or repetition.', |
| 70 | + '- Source-safety: NO raw source, code, diffs, secrets, or file contents appear in any ' + |
| 71 | + 'title, summary, measurementRationale, or patchSummary; flag any leakage.', |
| 72 | + '- Obfuscation/exclusion compliance: no coveredSourcePaths and no patch fileChanges ' + |
| 73 | + 'path touches an obfuscated path/concept (from the obfuscation guidance) or a ' + |
| 74 | + 'protected-IP exclusion; flag any violation by path.', |
| 75 | + '- Patch coherence: each pack has a source-safe patch descriptor with a non-empty ' + |
| 76 | + 'fileChanges list (path + op = create | modify | delete) and a patchSummary; flag ' + |
| 77 | + 'missing or incoherent patch descriptors.', |
| 78 | + '- Coverage: the packs adequately cover the repository\'s distinct, buyer-legible ' + |
| 79 | + 'knowledge as represented by the inventory; list the notable uncovered areas in ' + |
| 80 | + 'coverageGaps.', |
| 81 | + 'Set qualityScore in [0,1] as your overall honest quality of the synthesized supply. ' + |
| 82 | + 'Set recommendation to "iterate" when issues or material coverage gaps remain, else ' + |
| 83 | + '"complete". Return ONLY {"issues":[...],"qualityScore":n,"coverageGaps":[...],' + |
| 84 | + '"recommendation":"complete"|"iterate"}.', |
| 85 | + ].join('\n'), |
| 86 | +); |
| 87 | + |
| 88 | +const PLAN = part('Plan: enumerate the synthesized AssetPacks and the quality dimensions to check.'); |
| 89 | +const TRY = part('Try: run each quality, distinctness, source-safety, compliance, patch, and coverage check.'); |
| 90 | +const REFINE = part('Refine: keep only concrete, source-safe issues and an honest qualityScore and recommendation.'); |
| 91 | +const RETRY = part('Retry: when evidence is thin, validate the available AssetPack state and name what is missing.'); |
| 92 | + |
| 93 | +function createPrompt(): Prompt { |
| 94 | + const prompt = new Prompt(); |
| 95 | + prompt.set('agent:identity', IDENTITY); |
| 96 | + prompt.set('agent:requirements', REQUIREMENTS); |
| 97 | + prompt.set('ptrr:plan', PLAN); |
| 98 | + prompt.set('ptrr:try', TRY); |
| 99 | + prompt.set('ptrr:refine', REFINE); |
| 100 | + prompt.set('ptrr:retry', RETRY); |
| 101 | + prompt.require('agent:identity'); |
| 102 | + prompt.require('agent:requirements'); |
| 103 | + prompt.requirePattern('ptrr:*'); |
| 104 | + return prompt; |
| 105 | +} |
| 106 | + |
| 107 | +const prompt = createPrompt(); |
| 108 | + |
| 109 | +export const DepositValidationAgent = factoryAgentWithPTRR< |
| 110 | + z.infer<typeof DepositValidationInputSchema>, |
| 111 | + DepositValidationResult |
| 112 | +>({ |
| 113 | + name: 'DepositValidationAgent', |
| 114 | + description: |
| 115 | + 'Validates the synthesized deposit AssetPacks for quality, distinctness, source-safety, obfuscation/exclusion compliance, patch coherence, and coverage (deposit lens).', |
| 116 | + outputSchema: DepositValidationOutputSchema, |
| 117 | + tools: [], |
| 118 | + prompt, |
| 119 | + stepPrompts: { |
| 120 | + plan: () => prompt, |
| 121 | + try: () => prompt, |
| 122 | + refine: () => prompt, |
| 123 | + retry: () => prompt, |
| 124 | + }, |
| 125 | + plan: { chunkThreshold: 2000 }, |
| 126 | + try: { chunkThreshold: 4000 }, |
| 127 | + refine: { maxAttempts: 2 }, |
| 128 | + retry: { maxAttempts: 1 }, |
| 129 | +}); |
| 130 | + |
| 131 | +function findValue(execution: any, namespace: string, key: string): any { |
| 132 | + const local = execution?.get?.(namespace, key); |
| 133 | + if (local !== undefined) return local; |
| 134 | + return execution?.findUp?.(namespace, key); |
| 135 | +} |
| 136 | + |
| 137 | +function isNum01(value: unknown): boolean { |
| 138 | + return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1; |
| 139 | +} |
| 140 | + |
| 141 | +function asPathList(value: unknown): string[] { |
| 142 | + if (!Array.isArray(value)) return []; |
| 143 | + return value.filter((p): p is string => typeof p === 'string' && p.length > 0); |
| 144 | +} |
| 145 | + |
| 146 | +// A path violates an exclusion/obfuscation entry when it equals it or sits beneath |
| 147 | +// it as a directory prefix. Source-safe: operates on paths only, never contents. |
| 148 | +function pathViolates(path: string, entry: string): boolean { |
| 149 | + if (!path || !entry) return false; |
| 150 | + if (path === entry) return true; |
| 151 | + const dir = entry.endsWith('/') ? entry : `${entry}/`; |
| 152 | + return path.startsWith(dir); |
| 153 | +} |
| 154 | + |
| 155 | +// Deterministic AssetPack smoke/sanity checks over the source-safe descriptors. |
| 156 | +// These ground the gate so structurally broken or non-compliant synthesis output |
| 157 | +// always surfaces an issue, independent of the model's qualitative pass. |
| 158 | +function smokeCheckAssetPacks( |
| 159 | + assetPacks: any[], |
| 160 | + protectedIpExclusions: string[], |
| 161 | + obfuscatedPaths: string[], |
| 162 | +): string[] { |
| 163 | + const issues: string[] = []; |
| 164 | + if (!Array.isArray(assetPacks) || assetPacks.length === 0) { |
| 165 | + issues.push('No AssetPacks were synthesized to validate.'); |
| 166 | + return issues; |
| 167 | + } |
| 168 | + const forbidden = [...protectedIpExclusions, ...obfuscatedPaths]; |
| 169 | + const seenTitles = new Map<string, number>(); |
| 170 | + |
| 171 | + assetPacks.forEach((pack: any, index: number) => { |
| 172 | + const label = pack?.title ? `"${pack.title}"` : `#${index + 1}`; |
| 173 | + |
| 174 | + if (!isNum01(pack?.confidence)) { |
| 175 | + issues.push(`AssetPack ${label} has a missing or out-of-range confidence (expected 0..1).`); |
| 176 | + } |
| 177 | + |
| 178 | + const measurements = pack?.measurements; |
| 179 | + if (!measurements || typeof measurements !== 'object' || Array.isArray(measurements)) { |
| 180 | + issues.push(`AssetPack ${label} is missing its 0..1 measurements object.`); |
| 181 | + } else { |
| 182 | + for (const [key, value] of Object.entries(measurements)) { |
| 183 | + if (!isNum01(value)) { |
| 184 | + issues.push(`AssetPack ${label} measurement "${key}" is not an honest 0..1 volume.`); |
| 185 | + } |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + const coveredPaths = asPathList(pack?.coveredSourcePaths); |
| 190 | + if (coveredPaths.length === 0) { |
| 191 | + issues.push(`AssetPack ${label} declares no coveredSourcePaths.`); |
| 192 | + } |
| 193 | + |
| 194 | + const fileChanges = pack?.patch?.fileChanges; |
| 195 | + if (!Array.isArray(fileChanges) || fileChanges.length === 0) { |
| 196 | + issues.push(`AssetPack ${label} has no patch descriptor with fileChanges (patch coherence).`); |
| 197 | + } else if (typeof pack?.patch?.patchSummary !== 'string' || pack.patch.patchSummary.length === 0) { |
| 198 | + issues.push(`AssetPack ${label} patch descriptor is missing a source-safe patchSummary.`); |
| 199 | + } |
| 200 | + |
| 201 | + // Obfuscation/exclusion compliance over covered paths and patch fileChange paths. |
| 202 | + const patchPaths = Array.isArray(fileChanges) |
| 203 | + ? fileChanges.map((fc: any) => fc?.path).filter((p: any): p is string => typeof p === 'string') |
| 204 | + : []; |
| 205 | + for (const path of [...coveredPaths, ...patchPaths]) { |
| 206 | + const hit = forbidden.find((entry) => pathViolates(path, entry)); |
| 207 | + if (hit) { |
| 208 | + issues.push(`AssetPack ${label} touches withheld path "${path}" (violates exclusion/obfuscation "${hit}").`); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + if (typeof pack?.title === 'string' && pack.title.length > 0) { |
| 213 | + const norm = pack.title.trim().toLowerCase(); |
| 214 | + seenTitles.set(norm, (seenTitles.get(norm) ?? 0) + 1); |
| 215 | + } |
| 216 | + }); |
| 217 | + |
| 218 | + for (const [title, count] of seenTitles) { |
| 219 | + if (count > 1) { |
| 220 | + issues.push(`AssetPacks are not distinct: ${count} packs share the title "${title}".`); |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + return issues; |
| 225 | +} |
| 226 | + |
| 227 | +function dedupe(values: string[]): string[] { |
| 228 | + return Array.from(new Set(values.filter((v) => typeof v === 'string' && v.length > 0))); |
| 229 | +} |
| 230 | + |
| 231 | +export default async function runDepositValidationAgent(input: any, execution: any) { |
| 232 | + const assetPacks = |
| 233 | + input?.assetPacks ?? |
| 234 | + findValue(execution, 'implementation', 'options') ?? |
| 235 | + findValue(execution, 'implementation', 'assetPacks') ?? |
| 236 | + []; |
| 237 | + const inventory = input?.inventory ?? findValue(execution, 'deposit', 'inventory'); |
| 238 | + const obfuscationGuidance = |
| 239 | + input?.obfuscationGuidance ?? findValue(execution, 'setup', 'inputComprehension'); |
| 240 | + const protectedIpExclusions = asPathList( |
| 241 | + input?.protectedIpExclusions ?? findValue(execution, 'deposit', 'protectedIpExclusions') ?? [], |
| 242 | + ); |
| 243 | + const obfuscatedPaths = asPathList((obfuscationGuidance as any)?.obfuscatedPaths); |
| 244 | + const packs = Array.isArray(assetPacks) ? assetPacks : []; |
| 245 | + |
| 246 | + const agentOutput = await DepositValidationAgent( |
| 247 | + { |
| 248 | + ...input, |
| 249 | + assetPacks: packs, |
| 250 | + inventory, |
| 251 | + inventoryPaths: inventory?.paths, |
| 252 | + obfuscationGuidance, |
| 253 | + protectedIpExclusions, |
| 254 | + }, |
| 255 | + execution, |
| 256 | + ); |
| 257 | + |
| 258 | + // Deterministic smoke/sanity checks ground the model's qualitative pass. |
| 259 | + const smokeIssues = smokeCheckAssetPacks(packs, protectedIpExclusions, obfuscatedPaths); |
| 260 | + |
| 261 | + // Default-fallback to a clean "complete" verdict when the agent returns nothing; |
| 262 | + // the deterministic smoke issues are always merged so the gate stays grounded. |
| 263 | + const structured = |
| 264 | + agentOutput && typeof agentOutput === 'object' && Array.isArray((agentOutput as any).issues); |
| 265 | + const base: DepositValidationResult = structured |
| 266 | + ? { |
| 267 | + issues: Array.isArray((agentOutput as any).issues) ? (agentOutput as any).issues : [], |
| 268 | + qualityScore: isNum01((agentOutput as any).qualityScore) ? (agentOutput as any).qualityScore : 1, |
| 269 | + coverageGaps: Array.isArray((agentOutput as any).coverageGaps) ? (agentOutput as any).coverageGaps : [], |
| 270 | + recommendation: (agentOutput as any).recommendation === 'iterate' ? 'iterate' : 'complete', |
| 271 | + } |
| 272 | + : { issues: [], qualityScore: 1, coverageGaps: [], recommendation: 'complete' }; |
| 273 | + |
| 274 | + const issues = dedupe([...base.issues, ...smokeIssues]); |
| 275 | + const result: DepositValidationResult = { |
| 276 | + issues, |
| 277 | + qualityScore: base.qualityScore, |
| 278 | + coverageGaps: base.coverageGaps, |
| 279 | + // Any concrete issue (qualitative or smoke) forces an iterate verdict. |
| 280 | + recommendation: issues.length > 0 ? 'iterate' : base.recommendation, |
| 281 | + }; |
| 282 | + |
| 283 | + try { |
| 284 | + // Feed the AssetPack ReadyToFinish gate: it reads validation/implementation:issues |
| 285 | + // (a bare string[]) to decide finish vs. review. Match that store exactly. |
| 286 | + execution.store('validation/implementation', 'issues', result.issues); |
| 287 | + // Record the full deposit-quality verdict for telemetry / the /deposit surface. |
| 288 | + execution.store('validation', 'depositQuality', result); |
| 289 | + } catch {} |
| 290 | + |
| 291 | + return { ...(input || {}), ...result }; |
| 292 | +} |
0 commit comments