{"version":3,"file":"candidates.d.ts","sourceRoot":"","sources":["../../../src/core/routing/candidates.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACX,iBAAiB,EACjB,sBAAsB,EACtB,+BAA+B,EAC/B,eAAe,EACf,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,eAAe;IAC/B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,WAAW,GAAG,YAAY,GAAG,MAAM,CAAC;IACnD,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACzC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9C,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,uBAAuB,EAAE,eAAe,CAAC;CACzC,GAAG;IAAE,UAAU,EAAE,sBAAsB,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA0D/D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,UAAU,EAAE,sBAAsB,EAAE,EACpC,KAAK,EAAE,eAAe,GACpB;IAAE,QAAQ,EAAE,sBAAsB,EAAE,CAAC;IAAC,QAAQ,EAAE,+BAA+B,EAAE,CAAA;CAAE,CAwErF","sourcesContent":["/**\n * Candidate generation and hard policy filtering.\n *\n * Candidates are generated ONLY from configured and authorized components\n * (provider profiles, model registry, subagent registry, skill registry,\n * retrieval policies, budget classes). The engine never invents a provider,\n * model, skill or agent. Hard-policy filtering runs before scoring and cannot\n * be overcome by a higher evaluation score.\n */\n\nimport type {\n\tExecutionTopology,\n\tOrchestrationCandidate,\n\tOrchestrationCandidateRejection,\n\tSelectionPolicy,\n} from \"./types.js\";\n\nexport interface HardPolicyInput {\n\tworkspaceBoundary: boolean;\n\trequiredLocalOnly: boolean;\n\tproviderAllowlist: readonly string[];\n\tmodelAllowlist: readonly string[];\n\tmaxCostUsd?: number;\n\tmaxModelCalls?: number;\n\tmaxSubagents?: number;\n\tmaxAffectedFiles?: number;\n\tnetworkPolicy: \"allow_all\" | \"local_only\" | \"none\";\n\tallowLiveProviders: boolean;\n}\n\nexport interface CandidateSource {\n\tproviderProfile: string;\n\tconfiguredModel: string;\n\tthinkingLevel?: string;\n\texecutionTopology: ExecutionTopology;\n\tskillIds: string[];\n\tsubagentDefinitions: string[];\n\tretrievalPolicy: string;\n\tbudgetClass: string;\n\tfallbackPolicy: string;\n}\n\n/** Build the candidate cartesian set from configured components, then intersect permission. */\nexport function generateCandidates(input: {\n\tproviderProfiles: string[];\n\tmodels: { provider: string; model: string }[];\n\ttopologies: ExecutionTopology[];\n\tskills: string[];\n\tsubagents: string[];\n\tretrievalPolicies: string[];\n\tbudgetClasses: string[];\n\tfallbackPolicies: string[];\n\toperatorSelectionPolicy: SelectionPolicy;\n}): { candidates: OrchestrationCandidate[]; warnings: string[] } {\n\tconst {\n\t\tproviderProfiles,\n\t\tmodels,\n\t\ttopologies,\n\t\tskills,\n\t\tsubagents,\n\t\tretrievalPolicies,\n\t\tbudgetClasses,\n\t\tfallbackPolicies,\n\t} = input;\n\tconst warnings: string[] = [];\n\n\tif (providerProfiles.length === 0) {\n\t\twarnings.push(\"No provider profiles configured; falling back to single local analytical provider.\");\n\t}\n\n\t// Bounded cartesian product. Intersect each model with the provider profile\n\t// name so a candidate can never pair a model with a foreign provider profile.\n\tconst candidates: OrchestrationCandidate[] = [];\n\tconst providers = providerProfiles.length > 0 ? providerProfiles : [\"local\"];\n\tconst effectiveModels = models.length > 0 ? models : providers.map((p) => ({ provider: p, model: \"default\" }));\n\tconst topo: ExecutionTopology[] = topologies.length > 0 ? topologies : [\"single_agent\"];\n\tconst retrieval = retrievalPolicies.length > 0 ? retrievalPolicies : [\"hybrid\"];\n\tconst budget = budgetClasses.length > 0 ? budgetClasses : [\"standard\"];\n\tconst fallback = fallbackPolicies.length > 0 ? fallbackPolicies : [\"validated_policy\"];\n\n\tfor (const provider of providers) {\n\t\t// Pick a model that belongs to this provider when available, else first.\n\t\tconst owned = effectiveModels.find((m) => m.provider === provider);\n\t\tconst model = owned ?? effectiveModels[0];\n\t\tfor (const topology of topo) {\n\t\t\tfor (const rp of retrieval) {\n\t\t\t\tfor (const bc of budget) {\n\t\t\t\t\tfor (const fb of fallback) {\n\t\t\t\t\t\tconst candidateId = `c-${provider.replace(/[^a-z0-9]/gi, \"-\")}-${model.model.replace(/[^a-z0-9]/gi, \"-\")}-${topology}-${rp}-${bc}`;\n\t\t\t\t\t\tcandidates.push({\n\t\t\t\t\t\t\tcandidateId,\n\t\t\t\t\t\t\tproviderProfile: provider,\n\t\t\t\t\t\t\tconfiguredModel: model.model,\n\t\t\t\t\t\t\texecutionTopology: topology,\n\t\t\t\t\t\t\tskillIds: topology === \"custom_skill\" ? skills.slice(0, 1) : [],\n\t\t\t\t\t\t\tsubagentDefinitions:\n\t\t\t\t\t\t\t\ttopology === \"cavecrew\"\n\t\t\t\t\t\t\t\t\t? subagents.slice(0, 4)\n\t\t\t\t\t\t\t\t\t: topology === \"single_agent_with_reviewer\"\n\t\t\t\t\t\t\t\t\t\t? subagents.slice(0, 1)\n\t\t\t\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\t\tretrievalPolicy: rp,\n\t\t\t\t\t\t\tbudgetClass: bc,\n\t\t\t\t\t\t\tfallbackPolicy: fb,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn { candidates, warnings };\n}\n\n/**\n * Apply hard policy filters. Returns the surviving candidates and a rejection\n * list for every filtered candidate. Filtering is deterministic and ordered.\n */\nexport function applyHardPolicy(\n\tcandidates: OrchestrationCandidate[],\n\tinput: HardPolicyInput,\n): { accepted: OrchestrationCandidate[]; rejected: OrchestrationCandidateRejection[] } {\n\tconst accepted: OrchestrationCandidate[] = [];\n\tconst rejected: OrchestrationCandidateRejection[] = [];\n\n\tfor (const candidate of candidates) {\n\t\tconst reasons: OrchestrationCandidateRejection[] = [];\n\n\t\tif (!input.workspaceBoundary) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"workspace_boundary_denied\",\n\t\t\t\tpolicyRuleId: \"rule-workspace-boundary\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\t\tif (input.requiredLocalOnly && isRemoteProvider(candidate.providerProfile)) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"remote_provider_denied_local_only\",\n\t\t\t\tpolicyRuleId: \"rule-local-only\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\t\tif (!isProviderAllowed(candidate.providerProfile, input.providerAllowlist)) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"provider_not_in_allowlist\",\n\t\t\t\tpolicyRuleId: \"rule-provider-allowlist\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\t\tif (!isModelAllowed(candidate.configuredModel, input.modelAllowlist)) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"model_not_in_allowlist\",\n\t\t\t\tpolicyRuleId: \"rule-model-allowlist\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\t\tif (input.networkPolicy !== \"allow_all\" && isRemoteProvider(candidate.providerProfile)) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"network_policy_denies_remote\",\n\t\t\t\tpolicyRuleId: \"rule-network-policy\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\t\tif (!isLiveProviderAllowed(candidate.providerProfile, input.allowLiveProviders)) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"live_provider_not_authorized\",\n\t\t\t\tpolicyRuleId: \"rule-live-provider\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\t\tif (input.maxSubagents !== undefined && candidate.subagentDefinitions.length > input.maxSubagents) {\n\t\t\treasons.push({\n\t\t\t\tcandidateId: candidate.candidateId,\n\t\t\t\treasonCode: \"subagent_count_exceeds_max\",\n\t\t\t\tpolicyRuleId: \"rule-max-subagents\",\n\t\t\t\tevidenceIds: [],\n\t\t\t});\n\t\t}\n\n\t\tif (reasons.length > 0) {\n\t\t\trejected.push(...reasons);\n\t\t} else {\n\t\t\taccepted.push(candidate);\n\t\t}\n\t}\n\n\treturn { accepted, rejected };\n}\n\n/** Providers considered \"remote\" (require network / live credentials). */\nfunction isRemoteProvider(provider: string): boolean {\n\tconst p = provider.toLowerCase();\n\treturn !(p === \"local\" || p.startsWith(\"local-\") || p === \"fixture\" || p.includes(\"test\"));\n}\n\nfunction isProviderAllowed(provider: string, allowlist: readonly string[]): boolean {\n\tif (allowlist.length === 0) return true; // no allowlist = everything from registry allowed\n\treturn allowlist.includes(provider);\n}\n\nfunction isModelAllowed(model: string, allowlist: readonly string[]): boolean {\n\tif (allowlist.length === 0) return true;\n\treturn allowlist.includes(model);\n}\n\nfunction isLiveProviderAllowed(provider: string, allowLiveProviders: boolean): boolean {\n\tif (isRemoteProvider(provider)) return allowLiveProviders;\n\treturn true;\n}\n"]}