import type { ProposedShard } from "../types.js"; export interface ShardRejection { shard: ProposedShard; reason: string } export interface DecompositionResult { accepted: ProposedShard[]; rejected: ShardRejection[] } const norm = (value: string) => value.trim().toLowerCase().replace(/\s+/g, " "); const overlaps = (left: string, right: string) => { const a = left.replace(/\/+$/, ""); const b = right.replace(/\/+$/, ""); return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`); }; export function validateDecomposition(shards: ProposedShard[], remainingAgentBudget: number): DecompositionResult { const accepted: ProposedShard[] = []; const rejected: ShardRejection[] = []; const questions = new Set(); const evidence = new Set(); for (const shard of shards) { const missing = !shard.id || !shard.objective || !shard.lens || !shard.evidenceTarget || !shard.expectedOutput || shard.scope.length === 0; const duplicate = questions.has(norm(shard.objective)) || evidence.has(norm(shard.evidenceTarget)); const overlapsWriter = shard.writeIntent && accepted.some((other) => other.writeIntent && shard.scope.some((path) => other.scope.some((otherPath) => overlaps(path, otherPath)))); const invalid = missing ? "missing-required-field" : duplicate ? "duplicate-question-or-evidence" : !shard.canChangeFinalDecision ? "cannot-change-decision" : !shard.canRunIndependently ? "not-independent" : overlapsWriter ? "writer-scope-overlap" : accepted.length >= remainingAgentBudget ? "budget-exhausted" : undefined; if (invalid) { rejected.push({ shard, reason: invalid }); continue; } questions.add(norm(shard.objective)); evidence.add(norm(shard.evidenceTarget)); accepted.push(shard); } return { accepted, rejected }; }