{"version":3,"file":"capability-router.d.ts","sourceRoot":"","sources":["../../../src/core/capability-routing/capability-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAGX,mBAAmB,EACnB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+CAA+C,CAAC;AAC1F,OAAO,KAAK,EAAyB,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AAC5G,OAAO,KAAK,EACX,wBAAwB,EACxB,cAAc,EAEd,cAAc,EACd,iBAAiB,EACjB,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,mBAAmB;IACnC,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAwDD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB,GAAG,cAAc,CAwE/G;AAwBD,yEAAyE;AACzE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,mBAAmB,GAAG,cAAc,EAAE,CAIpF;AAMD,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,sBAAsB,CAAC;IAClC,kFAAkF;IAClF,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,gFAAgF;IAChF,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,qBAAa,gBAAiB,YAAW,wBAAwB;IAChE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAuB;IACjD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAoD;IACrF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IAEpC,YAAY,OAAO,EAAE,uBAAuB,EAK3C;IAED;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,mBAAmB,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAyD3G;YAEa,cAAc;YASd,MAAM;CAqBpB","sourcesContent":["/**\n * Capability Routing — deterministic router (2.15.0).\n *\n * Turns structured Mission requirements + a registry snapshot + worker liveness\n * + remote target health into explainable route candidates. The pure evaluator\n * (`evaluateRouteCandidates`) has no I/O and no process/transport side effects,\n * so requirements+snapshots → candidates is independently testable (§72). The\n * `CapabilityRouter` service gathers the snapshots and deduplicates remote\n * probes per target within a single evaluation (§56).\n *\n * This router never chooses the final executor. The Scheduler consumes the\n * candidates and applies its own deterministic policy. It also never routes\n * inference and never models shared-Qwen capacity.\n */\n\nimport type {\n\tCompatibilityRequirementItem,\n\tExecutionRouteMode,\n\tMissionRequirements,\n} from \"../assignment/assignment-types.js\";\nimport { evaluateCompatibility } from \"../assignment/compatibility.js\";\nimport type { ExecutorCapabilities } from \"../executor-registry/executor-registry-types.js\";\nimport type { ExecutorControlService } from \"../executor-registry/index.js\";\nimport type { RemoteTargetRegistry } from \"../remote-execution/remote-target-registry.js\";\nimport type { RemoteExecutionTarget, RemoteTargetHealth } from \"../remote-execution/remote-target-types.js\";\nimport type {\n\tCapabilityRouteEvaluator,\n\tExecutionRoute,\n\tRoutabilityStatus,\n\tRouteCandidate,\n\tRoutingEvaluation,\n} from \"./route-types.js\";\n\n// =============================================================================\n// Pure deterministic evaluator\n// =============================================================================\n\nexport interface EvaluateRoutesInput {\n\trequirements: MissionRequirements;\n\troutes: readonly ExecutionRoute[];\n\tnow: number;\n}\n\nfunction formatObserved(observed: string | readonly string[] | undefined): string {\n\tif (observed === undefined) return \"unknown\";\n\tif (typeof observed === \"string\") return observed;\n\treturn observed.length > 0 ? observed.join(\",\") : \"(none)\";\n}\n\nfunction compatibilityReasons(items: readonly CompatibilityRequirementItem[]): string[] {\n\treturn items.map((item) => {\n\t\tif (item.kind === \"platform.os\") {\n\t\t\treturn item.observed === undefined\n\t\t\t\t? `platform.os \"${item.requirement}\" required but route platform is unknown`\n\t\t\t\t: `platform.os \"${item.requirement}\" required but route platform is \"${item.observed}\"`;\n\t\t}\n\t\tif (item.kind === \"platform.arch\") {\n\t\t\treturn item.observed === undefined\n\t\t\t\t? `platform.arch \"${item.requirement}\" required but route arch is unknown`\n\t\t\t\t: `platform.arch \"${item.requirement}\" required but route arch is \"${item.observed}\"`;\n\t\t}\n\t\tif (item.kind === \"provider\" || item.kind === \"model\") {\n\t\t\treturn `${item.kind} requirement \"${item.requirement}\" unsatisfied (observed ${formatObserved(item.observed)})`;\n\t\t}\n\t\treturn `${item.kind} capability \"${item.requirement}\" required but not advertised`;\n\t});\n}\n\nfunction preferenceFor(route: ExecutionRoute, requirements: MissionRequirements): { score: number; reasons: string[] } {\n\tconst prefs = requirements.preferences;\n\tlet score = 0;\n\tconst reasons: string[] = [];\n\tif (prefs?.executionMode !== undefined) {\n\t\tif (prefs.executionMode === route.executionMode) {\n\t\t\tscore += 1;\n\t\t\treasons.push(`execution mode \"${prefs.executionMode}\" preferred`);\n\t\t} else {\n\t\t\treasons.push(`execution mode \"${prefs.executionMode}\" preferred (route is \"${route.executionMode}\")`);\n\t\t}\n\t}\n\tif (prefs?.executorId !== undefined) {\n\t\tif (prefs.executorId === route.executorId) {\n\t\t\tscore += 1;\n\t\t\treasons.push(`executor \"${prefs.executorId}\" preferred`);\n\t\t}\n\t}\n\tif (prefs?.remoteTargetId !== undefined) {\n\t\tif (route.executionMode === \"remote\" && route.remoteTargetId === prefs.remoteTargetId) {\n\t\t\tscore += 1;\n\t\t\treasons.push(`remote target \"${prefs.remoteTargetId}\" preferred`);\n\t\t} else if (route.executionMode === \"remote\") {\n\t\t\treasons.push(`remote target \"${prefs.remoteTargetId}\" preferred (route target is \"${route.remoteTargetId}\")`);\n\t\t}\n\t}\n\treturn { score, reasons };\n}\n\n/**\n * Evaluate one fully-resolved route against hard requirements and availability.\n * Platform/arch for remote routes are authoritative from the target; the\n * executor's declared capabilities are merged only for execution capability\n * matching (never platform inference for a remote physical machine).\n */\nexport function evaluateRouteCandidate(route: ExecutionRoute, requirements: MissionRequirements): RouteCandidate {\n\tconst platformOverride =\n\t\troute.platform !== undefined || route.arch !== undefined\n\t\t\t? { os: route.platform, arch: route.arch }\n\t\t\t: route.capabilities.platform;\n\tconst effectiveCapabilities: ExecutorCapabilities = { ...route.capabilities, platform: platformOverride };\n\tconst compatibility = evaluateCompatibility(requirements, effectiveCapabilities);\n\n\tconst executionModeMatch =\n\t\trequirements.executionMode === undefined || route.executionMode === requirements.executionMode;\n\tconst platformMatch = !compatibility.unsatisfied.some((item) => item.kind === \"platform.os\");\n\tconst archMatch = !compatibility.unsatisfied.some((item) => item.kind === \"platform.arch\");\n\tconst capabilityMatch = compatibility.compatible && executionModeMatch;\n\n\tconst workerAvailable = !route.retired && route.status === \"ONLINE\";\n\tconst targetAvailable = route.executionMode === \"local\" ? true : route.targetHealth?.status === \"reachable\";\n\tconst eligible = capabilityMatch && workerAvailable && targetAvailable;\n\n\tconst rejectionReasons = compatibilityReasons(compatibility.unsatisfied);\n\tif (!executionModeMatch) {\n\t\trejectionReasons.unshift(\n\t\t\t`execution mode \"${requirements.executionMode}\" required but route is \"${route.executionMode}\"`,\n\t\t);\n\t}\n\tif (route.retired) rejectionReasons.push(\"executor is retired\");\n\telse if (!workerAvailable) rejectionReasons.push(`worker is not ONLINE (${route.status})`);\n\tif (!targetAvailable) {\n\t\tconst health = route.targetHealth;\n\t\trejectionReasons.push(\n\t\t\t`remote target \"${route.remoteTargetId ?? \"\"}\" is ${health?.status ?? \"unknown\"}${\n\t\t\t\thealth?.summary ? `: ${health.summary}` : \"\"\n\t\t\t}`,\n\t\t);\n\t}\n\n\tconst status = routabilityStatus({\n\t\texecutionModeMatch,\n\t\tplatformMatch,\n\t\tarchMatch,\n\t\tcapabilityMatch: compatibility.compatible,\n\t\tworkerAvailable,\n\t\ttargetAvailable,\n\t\trequirements,\n\t\troute,\n\t});\n\n\tconst preference = preferenceFor(route, requirements);\n\n\treturn {\n\t\texecutorId: route.executorId,\n\t\texecutionMode: route.executionMode,\n\t\tremoteTargetId: route.remoteTargetId,\n\t\ttransport: route.transport,\n\t\tplatform: route.platform,\n\t\tarch: route.arch,\n\t\teligible,\n\t\tstatus,\n\t\tcapabilityMatch,\n\t\tplatformMatch,\n\t\tarchMatch,\n\t\texecutionModeMatch,\n\t\tworkerAvailable,\n\t\ttargetAvailable,\n\t\tworkerStatus: route.status,\n\t\tretired: route.retired,\n\t\ttargetHealth: route.targetHealth,\n\t\tmatchedRequirements: compatibility.satisfied,\n\t\trejectedRequirements: compatibility.unsatisfied,\n\t\trejectionReasons,\n\t\tpreferenceScore: preference.score,\n\t\tpreferenceReasons: preference.reasons,\n\t};\n}\n\nfunction routabilityStatus(input: {\n\texecutionModeMatch: boolean;\n\tplatformMatch: boolean;\n\tarchMatch: boolean;\n\tcapabilityMatch: boolean;\n\tworkerAvailable: boolean;\n\ttargetAvailable: boolean;\n\trequirements: MissionRequirements;\n\troute: ExecutionRoute;\n}): RoutabilityStatus {\n\tif (!input.executionModeMatch) return \"INELIGIBLE_EXECUTION_MODE\";\n\tif (input.requirements.platform?.os !== undefined && input.route.platform === undefined)\n\t\treturn \"UNKNOWN_REQUIREMENT\";\n\tif (!input.platformMatch) return \"INELIGIBLE_PLATFORM\";\n\tif (input.requirements.platform?.arch !== undefined && input.route.arch === undefined) return \"UNKNOWN_REQUIREMENT\";\n\tif (!input.archMatch) return \"INELIGIBLE_ARCH\";\n\tif (!input.capabilityMatch) return \"INELIGIBLE_CAPABILITY\";\n\tif (!input.workerAvailable) return \"UNAVAILABLE_WORKER\";\n\tif (!input.targetAvailable) return \"UNAVAILABLE_TARGET\";\n\treturn \"ELIGIBLE\";\n}\n\n/** Deterministic candidate evaluation: ascending executorId ordering. */\nexport function evaluateRouteCandidates(input: EvaluateRoutesInput): RouteCandidate[] {\n\treturn input.routes\n\t\t.map((route) => evaluateRouteCandidate(route, input.requirements))\n\t\t.sort((a, b) => (a.executorId < b.executorId ? -1 : a.executorId > b.executorId ? 1 : 0));\n}\n\n// =============================================================================\n// Service\n// =============================================================================\n\nexport interface CapabilityRouterOptions {\n\texecutors: ExecutorControlService;\n\t/** Optional target registry for resolving remote route platform/arch + probes. */\n\ttargets?: RemoteTargetRegistry;\n\t/** Injected health provider (deterministic tests); defaults to target probe. */\n\thealthProvider?: (targetId: string) => Promise<RemoteTargetHealth>;\n\tnow?: () => number;\n}\n\nexport class CapabilityRouter implements CapabilityRouteEvaluator {\n\tprivate readonly _executors: ExecutorControlService;\n\tprivate readonly _targets?: RemoteTargetRegistry;\n\tprivate readonly _healthProvider?: (targetId: string) => Promise<RemoteTargetHealth>;\n\tprivate readonly _now: () => number;\n\n\tconstructor(options: CapabilityRouterOptions) {\n\t\tthis._executors = options.executors;\n\t\tthis._targets = options.targets;\n\t\tthis._healthProvider = options.healthProvider;\n\t\tthis._now = options.now ?? (() => Date.now());\n\t}\n\n\t/**\n\t * Gather registry/worker/target snapshots, resolve remote routes (platform,\n\t * arch, transport from the target) and deduplicate target probes, then run\n\t * the pure evaluator.\n\t */\n\tasync evaluate(input: { requirements: MissionRequirements; missionId?: string }): Promise<RoutingEvaluation> {\n\t\tconst now = this._now();\n\t\tconst executorList = await this._executors.listExecutors();\n\n\t\tconst remoteTargetIds = new Set<string>();\n\t\tconst routes: ExecutionRoute[] = executorList.entries.map((summary) => {\n\t\t\tconst executionMode: ExecutionRouteMode = summary.remoteTargetId ? \"remote\" : \"local\";\n\t\t\tif (summary.remoteTargetId) remoteTargetIds.add(summary.remoteTargetId);\n\t\t\treturn {\n\t\t\t\texecutorId: summary.executorId,\n\t\t\t\texecutionMode,\n\t\t\t\tremoteTargetId: summary.remoteTargetId,\n\t\t\t\tplatform: summary.platform ?? summary.capabilities.platform?.os,\n\t\t\t\tarch: summary.arch ?? summary.capabilities.platform?.arch,\n\t\t\t\tcapabilities: summary.capabilities,\n\t\t\t\tstatus: summary.status,\n\t\t\t\tretired: summary.retired,\n\t\t\t};\n\t\t});\n\n\t\tconst orderedTargetIds = [...remoteTargetIds].sort();\n\t\tconst targetById = new Map<string, RemoteExecutionTarget>();\n\t\tconst targetCorrupt: RoutingEvaluation[\"targetCorrupt\"] = [];\n\t\tfor (const targetId of orderedTargetIds) {\n\t\t\tconst resolved = await this._resolveTarget(targetId);\n\t\t\tif (resolved.target) targetById.set(targetId, resolved.target);\n\t\t\telse targetCorrupt.push({ targetId, diagnostic: resolved.diagnostic });\n\t\t}\n\n\t\tconst targetHealth = new Map<string, RemoteTargetHealth>();\n\t\tfor (const targetId of orderedTargetIds) {\n\t\t\ttargetHealth.set(targetId, await this._probe(targetId));\n\t\t}\n\n\t\tconst resolvedRoutes: ExecutionRoute[] = routes.map((route) => {\n\t\t\tif (route.executionMode !== \"remote\" || !route.remoteTargetId) return route;\n\t\t\tconst target = targetById.get(route.remoteTargetId);\n\t\t\treturn {\n\t\t\t\t...route,\n\t\t\t\tplatform: target?.platform ?? route.platform,\n\t\t\t\tarch: target?.arch ?? route.arch,\n\t\t\t\ttransport: target?.transport,\n\t\t\t\ttargetHealth: targetHealth.get(route.remoteTargetId),\n\t\t\t};\n\t\t});\n\n\t\tconst candidates = evaluateRouteCandidates({ requirements: input.requirements, routes: resolvedRoutes, now });\n\t\treturn {\n\t\t\tmissionId: input.missionId,\n\t\t\trequirements: input.requirements,\n\t\t\tcandidates,\n\t\t\teligibleCount: candidates.filter((candidate) => candidate.eligible).length,\n\t\t\teligibleExecutorIds: candidates.filter((candidate) => candidate.eligible).map((c) => c.executorId),\n\t\t\tcorrupt: executorList.corrupt,\n\t\t\ttargetCorrupt,\n\t\t\tevaluatedAtMs: now,\n\t\t};\n\t}\n\n\tprivate async _resolveTarget(targetId: string): Promise<{ target?: RemoteExecutionTarget; diagnostic: string }> {\n\t\tif (!this._targets) return { diagnostic: \"target registry not configured\" };\n\t\ttry {\n\t\t\treturn { target: await this._targets.get(targetId), diagnostic: \"\" };\n\t\t} catch (error) {\n\t\t\treturn { diagnostic: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t}\n\n\tprivate async _probe(targetId: string): Promise<RemoteTargetHealth> {\n\t\tif (this._healthProvider) return this._healthProvider(targetId);\n\t\tif (this._targets) {\n\t\t\ttry {\n\t\t\t\treturn await this._targets.probe(targetId);\n\t\t\t} catch (error) {\n\t\t\t\treturn {\n\t\t\t\t\ttargetId,\n\t\t\t\t\tstatus: \"unknown\",\n\t\t\t\t\tsummary: `probe failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t\t\tobservedAtMs: this._now(),\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\ttargetId,\n\t\t\tstatus: \"unknown\",\n\t\t\tsummary: \"no target registry/transport configured for probes\",\n\t\t\tobservedAtMs: this._now(),\n\t\t};\n\t}\n}\n"]}