{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGX,iBAAiB,EACjB,yBAAyB,EACzB,4BAA4B,EAC5B,6BAA6B,EAC7B,MAAM,YAAY,CAAC;AAmIpB;;;;;;;;GAQG;AACH,MAAM,WAAW,8BAA8B;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,8BAAmC,GAC1C,6BAA6B,CAsD/B;AAED,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,OAAO,GACZ;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,yBAAyB,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,4BAA4B,EAAE,CAAA;CAAE,CA2EjH","sourcesContent":["import { getCanonicalSubagentRegistry } from \"../subagent-registry.js\";\nimport type {\n\tOrchestrationEdge,\n\tOrchestrationNode,\n\tOrchestrationPlan,\n\tOrchestrationPlanProposal,\n\tOrchestrationValidationIssue,\n\tOrchestrationValidationResult,\n} from \"./types.js\";\n\nconst NODE_KINDS = new Set([\"DIRECT\", \"CHILD\", \"SYNTHESIS\", \"REVIEW\", \"VERIFICATION\"]);\nconst REQUIREMENTS = new Set([\"REQUIRED\", \"OPTIONAL\", \"VERIFICATION_GATING\"]);\nconst ACCESS = new Set([\"READ_ONLY\", \"WRITE\"]);\n\nfunction issue(\n\tissues: OrchestrationValidationIssue[],\n\tcode: OrchestrationValidationIssue[\"code\"],\n\tpath: string,\n\tmessage: string,\n): void {\n\tissues.push({ code, path, message });\n}\n\nfunction hasPath(edges: readonly OrchestrationEdge[], from: string, to: string): boolean {\n\tconst next = new Map<string, string[]>();\n\tfor (const edge of edges) next.set(edge.from, [...(next.get(edge.from) ?? []), edge.to]);\n\tconst seen = new Set<string>();\n\tconst stack = [from];\n\twhile (stack.length > 0) {\n\t\tconst current = stack.pop()!;\n\t\tif (current === to) return true;\n\t\tif (seen.has(current)) continue;\n\t\tseen.add(current);\n\t\tstack.push(...(next.get(current) ?? []));\n\t}\n\treturn false;\n}\n\nfunction dependencyCriticality(\n\tnodes: readonly OrchestrationNode[],\n\tedges: readonly OrchestrationEdge[],\n): Map<string, number> {\n\tconst counts = new Map<string, number>(nodes.map((node) => [node.nodeId, 0]));\n\tfor (const edge of edges) {\n\t\tif (edge.kind === \"REQUIRED\") counts.set(edge.from, (counts.get(edge.from) ?? 0) + 1);\n\t}\n\treturn counts;\n}\n\nfunction validateDag(\n\tnodes: readonly OrchestrationNode[],\n\tedges: readonly OrchestrationEdge[],\n\tissues: OrchestrationValidationIssue[],\n): void {\n\tconst ids = new Set(nodes.map((node) => node.nodeId));\n\tconst visiting = new Set<string>();\n\tconst visited = new Set<string>();\n\tconst adjacency = new Map<string, string[]>();\n\tfor (const edge of edges) {\n\t\tif (!ids.has(edge.from))\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_MISSING_DEPENDENCY\",\n\t\t\t\t`edges.${edge.from}`,\n\t\t\t\t`Unknown dependency source ${edge.from}`,\n\t\t\t);\n\t\tif (!ids.has(edge.to))\n\t\t\tissue(issues, \"ORCHESTRATION_MISSING_DEPENDENCY\", `edges.${edge.to}`, `Unknown dependency target ${edge.to}`);\n\t\tif (edge.from === edge.to)\n\t\t\tissue(issues, \"ORCHESTRATION_CYCLE\", `edges.${edge.from}`, \"A node cannot depend on itself\");\n\t\tadjacency.set(edge.from, [...(adjacency.get(edge.from) ?? []), edge.to]);\n\t}\n\tconst walk = (nodeId: string, path: string[]): void => {\n\t\tif (visiting.has(nodeId)) {\n\t\t\tissue(issues, \"ORCHESTRATION_CYCLE\", `nodes.${nodeId}`, `Dependency cycle: ${[...path, nodeId].join(\" -> \")}`);\n\t\t\treturn;\n\t\t}\n\t\tif (visited.has(nodeId)) return;\n\t\tvisiting.add(nodeId);\n\t\tfor (const next of adjacency.get(nodeId) ?? []) walk(next, [...path, nodeId]);\n\t\tvisiting.delete(nodeId);\n\t\tvisited.add(nodeId);\n\t};\n\tfor (const node of nodes) walk(node.nodeId, []);\n}\n\nfunction validateDuplicateWork(nodes: readonly OrchestrationNode[], issues: OrchestrationValidationIssue[]): void {\n\tconst seen = new Map<string, OrchestrationNode>();\n\tfor (const node of nodes) {\n\t\tconst key = JSON.stringify({\n\t\t\trole: node.role,\n\t\t\tobjective: node.objective.trim(),\n\t\t\tworkspaceKey: node.workspaceKey ?? \"\",\n\t\t\trequirements: node.requirements ?? {},\n\t\t});\n\t\tconst previous = seen.get(key);\n\t\tif (\n\t\t\tprevious &&\n\t\t\tnode.independenceReason !== \"independent_review\" &&\n\t\t\tnode.independenceReason !== \"uncertainty_reduction\"\n\t\t) {\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_DUPLICATE_WORK\",\n\t\t\t\t`nodes.${node.nodeId}`,\n\t\t\t\t`Duplicates node ${previous.nodeId} without an explicit independence rationale`,\n\t\t\t);\n\t\t}\n\t\tseen.set(key, node);\n\t}\n}\n\nfunction validateWriterSafety(\n\tplan: Pick<OrchestrationPlanProposal, \"nodes\" | \"edges\">,\n\tissues: OrchestrationValidationIssue[],\n): void {\n\tfor (let i = 0; i < plan.nodes.length; i++) {\n\t\tfor (let j = i + 1; j < plan.nodes.length; j++) {\n\t\t\tconst left = plan.nodes[i];\n\t\t\tconst right = plan.nodes[j];\n\t\t\tif (\n\t\t\t\tleft.workspaceAccess !== \"WRITE\" ||\n\t\t\t\tright.workspaceAccess !== \"WRITE\" ||\n\t\t\t\t!left.workspaceKey ||\n\t\t\t\tleft.workspaceKey !== right.workspaceKey\n\t\t\t)\n\t\t\t\tcontinue;\n\t\t\tif (!hasPath(plan.edges, left.nodeId, right.nodeId) && !hasPath(plan.edges, right.nodeId, left.nodeId)) {\n\t\t\t\tissue(\n\t\t\t\t\tissues,\n\t\t\t\t\t\"ORCHESTRATION_UNSAFE_WRITERS\",\n\t\t\t\t\t`nodes.${right.nodeId}`,\n\t\t\t\t\t`Concurrent writers share workspace ${left.workspaceKey} without a dependency ordering`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Validation options for `validateOrchestrationPlan`.\n *\n * `operatorAgents` is the explicit operator-set hook: node agents may\n * additionally resolve to the supplied operator names (for example the\n * canonical operator roster names exposed by the Qwen planner). The canonical\n * subagent registry remains the primary authority; it is only extended by an\n * explicitly supplied set, never by ambient configuration.\n */\nexport interface OrchestrationValidationOptions {\n\tparentDepth?: number;\n\t/** Explicit operator agent names accepted on top of the canonical registry. */\n\toperatorAgents?: readonly string[];\n}\n\nexport function validateOrchestrationPlan(\n\tplan: OrchestrationPlan,\n\toptions: OrchestrationValidationOptions = {},\n): OrchestrationValidationResult {\n\tconst issues: OrchestrationValidationIssue[] = [];\n\tconst operatorAgents = options.operatorAgents ? new Set(options.operatorAgents) : undefined;\n\tif (!plan.orchestrationId || !plan.parentMissionId)\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"plan\", \"orchestrationId and parentMissionId are required\");\n\tif (\n\t\tplan.revision < 1 ||\n\t\tplan.maxDepth < 0 ||\n\t\tplan.maxChildrenPerNode < 1 ||\n\t\tplan.maxTotalLogicalAgents < 1 ||\n\t\tplan.maxReplans < 0\n\t)\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"plan.limits\", \"Plan limits are invalid\");\n\tif (options.parentDepth !== undefined && options.parentDepth + 1 > plan.maxDepth)\n\t\tissue(issues, \"ORCHESTRATION_DEPTH_EXCEEDED\", \"plan.maxDepth\", \"Plan exceeds the configured recursion depth\");\n\tif (plan.decision === \"DIRECT\" && plan.nodes.length > 0)\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"plan.nodes\", \"DIRECT plans must not contain child nodes\");\n\tif (plan.decision === \"FANOUT\" && plan.nodes.length === 0)\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"plan.nodes\", \"FANOUT plans require at least one child node\");\n\tif (plan.nodes.length > plan.maxChildrenPerNode || plan.nodes.length > plan.maxTotalLogicalAgents)\n\t\tissue(issues, \"ORCHESTRATION_CHILD_LIMIT\", \"plan.nodes\", \"Plan exceeds logical-agent safety limits\");\n\tconst ids = new Set<string>();\n\tfor (const [index, node] of plan.nodes.entries()) {\n\t\tif (!node.nodeId || ids.has(node.nodeId))\n\t\t\tissue(issues, \"ORCHESTRATION_DUPLICATE_NODE\", `nodes.${index}.nodeId`, `Duplicate node id ${node.nodeId}`);\n\t\tids.add(node.nodeId);\n\t\tif (!NODE_KINDS.has(node.nodeKind))\n\t\t\tissue(issues, \"ORCHESTRATION_INVALID_ROLE\", `nodes.${node.nodeId}.nodeKind`, \"Unknown node kind\");\n\t\tif (!REQUIREMENTS.has(node.requirement))\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_INVALID_REQUIREMENT\",\n\t\t\t\t`nodes.${node.nodeId}.requirement`,\n\t\t\t\t\"Unknown node requirement\",\n\t\t\t);\n\t\tif (!ACCESS.has(node.workspaceAccess))\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_PLAN_INVALID\",\n\t\t\t\t`nodes.${node.nodeId}.workspaceAccess`,\n\t\t\t\t\"Unknown workspace access\",\n\t\t\t);\n\t\tif (!node.objective.trim() || !node.role.trim() || !node.agent.trim())\n\t\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", `nodes.${node.nodeId}`, \"role, agent, and objective are required\");\n\t\tconst resolvedAgent = getCanonicalSubagentRegistry().resolve(node.agent);\n\t\t// Roster-only names are legal only through the explicit operator set;\n\t\t// the canonical registry stays the primary authority.\n\t\tif (\"code\" in resolvedAgent && !operatorAgents?.has(node.agent))\n\t\t\tissue(issues, \"ORCHESTRATION_INVALID_ROLE\", `nodes.${node.nodeId}.agent`, `Unknown agent ${node.agent}`);\n\t}\n\tvalidateDag(plan.nodes, plan.edges, issues);\n\tvalidateDuplicateWork(plan.nodes, issues);\n\tvalidateWriterSafety(plan, issues);\n\treturn { valid: issues.length === 0, issues, dependencyCriticality: dependencyCriticality(plan.nodes, plan.edges) };\n}\n\nexport function validatePlanProposal(\n\tvalue: unknown,\n): { valid: true; proposal: OrchestrationPlanProposal } | { valid: false; issues: OrchestrationValidationIssue[] } {\n\tconst issues: OrchestrationValidationIssue[] = [];\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"proposal\", \"Proposal must be an object\");\n\t\treturn { valid: false, issues };\n\t}\n\tconst raw = value as Record<string, unknown>;\n\tif (raw.decision !== \"DIRECT\" && raw.decision !== \"FANOUT\")\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"proposal.decision\", \"decision must be DIRECT or FANOUT\");\n\tif (typeof raw.rationale !== \"string\")\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"proposal.rationale\", \"rationale is required\");\n\tif (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) {\n\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", \"proposal\", \"nodes and edges must be arrays\");\n\t\treturn { valid: false, issues };\n\t}\n\tfor (const [index, value] of raw.nodes.entries()) {\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", `proposal.nodes.${index}`, \"node must be an object\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst node = value as Record<string, unknown>;\n\t\tfor (const field of [\n\t\t\t\"nodeId\",\n\t\t\t\"role\",\n\t\t\t\"nodeKind\",\n\t\t\t\"objective\",\n\t\t\t\"agent\",\n\t\t\t\"executionMode\",\n\t\t\t\"requirement\",\n\t\t\t\"workspaceAccess\",\n\t\t\t\"status\",\n\t\t] as const) {\n\t\t\tif (typeof node[field] !== \"string\" || node[field].trim().length === 0)\n\t\t\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", `proposal.nodes.${index}.${field}`, `${field} is required`);\n\t\t}\n\t\tif (!Array.isArray(node.acceptanceCriteria))\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_PLAN_INVALID\",\n\t\t\t\t`proposal.nodes.${index}.acceptanceCriteria`,\n\t\t\t\t\"acceptanceCriteria must be an array\",\n\t\t\t);\n\t\tif (node.independenceReason !== undefined && typeof node.independenceReason !== \"string\")\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_PLAN_INVALID\",\n\t\t\t\t`proposal.nodes.${index}.independenceReason`,\n\t\t\t\t\"independenceReason must be a string\",\n\t\t\t);\n\t}\n\tfor (const [index, value] of raw.edges.entries()) {\n\t\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\t\tissue(issues, \"ORCHESTRATION_PLAN_INVALID\", `proposal.edges.${index}`, \"edge must be an object\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst edge = value as Record<string, unknown>;\n\t\tif (\n\t\t\ttypeof edge.from !== \"string\" ||\n\t\t\ttypeof edge.to !== \"string\" ||\n\t\t\t(edge.kind !== \"REQUIRED\" && edge.kind !== \"OPTIONAL\")\n\t\t)\n\t\t\tissue(\n\t\t\t\tissues,\n\t\t\t\t\"ORCHESTRATION_PLAN_INVALID\",\n\t\t\t\t`proposal.edges.${index}`,\n\t\t\t\t\"edge requires from, to, and valid kind\",\n\t\t\t);\n\t}\n\tconst proposal = {\n\t\tdecision: raw.decision,\n\t\trationale: raw.rationale,\n\t\tnodes: raw.nodes,\n\t\tedges: raw.edges,\n\t} as OrchestrationPlanProposal;\n\treturn issues.length === 0 ? { valid: true, proposal } : { valid: false, issues };\n}\n"]}