{"version":3,"file":"promotion.d.ts","sourceRoot":"","sources":["../../../src/core/routing/promotion.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACN,KAAK,mBAAmB,EAOxB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE5E,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,gFAAgF;AAChF,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,sBAAsB,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC/C,KAAK,EAAE,kBAAkB,GACvB,mBAAmB,CAsCrB;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EAC/C,KAAK,EAAE,kBAAkB,GACvB;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CA0BvF;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAkBtG;AAED,8CAA8C;AAC9C,wBAAgB,eAAe,CAAC,CAAC,EAAE,sBAAsB,EAAE,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAM7F","sourcesContent":["/**\n * Policy promotion and rollback.\n *\n * Promotion is explicitly gated: schema validation, dataset integrity,\n * baseline comparison, safety/correctness/flakiness gates, cost/latency policy,\n * and operator authorization. There is NO automatic promotion. The old policy is\n * always retained and the active-policy pointer is swapped atomically.\n */\n\nimport {\n\ttype ActivePolicyPointer,\n\tappendEvent,\n\tloadActivePolicyPointer,\n\treadPolicy,\n\tsaveActivePolicyPointer,\n\tsha256,\n\tstableStringify,\n} from \"./store.js\";\nimport type { CandidateEvidence, RoutingPolicyCandidate } from \"./types.js\";\n\nexport interface PromotionGateInput {\n\tsafetyFloor: number;\n\tcorrectnessFloor: number;\n\tflakinessCeiling: number;\n\trequiredScenarioPack: string;\n\toperatorAuthorized: boolean;\n}\n\nexport interface PromotionGateResult {\n\tpassed: boolean;\n\treasonCodes: string[];\n}\n\n/** Validate the promotion gates for a candidate policy against its evidence. */\nexport function validatePromotionGates(\n\tpolicy: RoutingPolicyCandidate,\n\tevidenceById: Record<string, CandidateEvidence>,\n\tgates: PromotionGateInput,\n): PromotionGateResult {\n\tconst reasons: string[] = [];\n\n\tif (policy.status !== \"draft\" && policy.status !== \"validated\") {\n\t\treasons.push(`policy_status_${policy.status}_not_promotable`);\n\t}\n\tif (policy.hash !== sha256(policy.content ?? stableStringify(policy))) {\n\t\treasons.push(\"policy_hash_mismatch\");\n\t}\n\n\tconst prefs = policy.preferences ?? [];\n\tif (prefs.length === 0) {\n\t\treasons.push(\"policy_has_no_preferences\");\n\t}\n\n\tfor (const pref of prefs) {\n\t\tconst ev = evidenceById[pref.candidateId];\n\t\tif (!ev) {\n\t\t\treasons.push(`no_evidence_for_${pref.candidateId}`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (ev.sampleCount < 5) reasons.push(`insufficient_samples_${pref.candidateId}`);\n\t\tif ((ev.safetyRate ?? 0) < gates.safetyFloor) reasons.push(`safety_gate_failed_${pref.candidateId}`);\n\t\tif ((ev.correctnessRate ?? 0) < gates.correctnessFloor)\n\t\t\treasons.push(`correctness_gate_failed_${pref.candidateId}`);\n\t\tif ((ev.flakyRate ?? 0) > gates.flakinessCeiling) reasons.push(`flakiness_gate_failed_${pref.candidateId}`);\n\t}\n\n\tif (!gates.operatorAuthorized) reasons.push(\"operator_not_authorized\");\n\n\tappendEvent({\n\t\ttype: \"ROUTING_POLICY_VALIDATED\",\n\t\tpolicyId: policy.policyId,\n\t\tpolicyVersion: policy.policyVersion,\n\t\tpayload: { passed: reasons.length === 0, reasonCodes: reasons },\n\t});\n\n\treturn { passed: reasons.length === 0, reasonCodes: reasons };\n}\n\n/** Promote a validated policy to be the active policy (explicit operator action). */\nexport function promotePolicy(\n\tpolicyId: string,\n\tauthorizedBy: string,\n\tevidenceById: Record<string, CandidateEvidence>,\n\tgates: PromotionGateInput,\n): { ok: boolean; pointer?: ActivePolicyPointer; error?: string; reasonCodes: string[] } {\n\tconst policy = readPolicy(policyId);\n\tif (!policy) return { ok: false, error: `policy ${policyId} not found`, reasonCodes: [\"policy_not_found\"] };\n\n\tconst gate = validatePromotionGates(policy, evidenceById, gates);\n\tif (!gate.passed) {\n\t\treturn { ok: false, error: \"promotion gates failed\", reasonCodes: gate.reasonCodes };\n\t}\n\n\tconst prev = loadActivePolicyPointer();\n\tpolicy.status = \"promoted\";\n\tconst pointer = {\n\t\tpolicyId: policy.policyId,\n\t\tpolicyVersion: policy.policyVersion,\n\t\thash: policy.hash,\n\t\tactivatedAt: new Date().toISOString(),\n\t\tpreviousPolicyId: prev?.policyId,\n\t};\n\tsaveActivePolicyPointer(pointer);\n\tappendEvent({\n\t\ttype: \"ROUTING_POLICY_PROMOTED\",\n\t\tpolicyId: policy.policyId,\n\t\tpolicyVersion: policy.policyVersion,\n\t\tpayload: { authorizedBy, previousPolicyId: prev?.policyId },\n\t});\n\treturn { ok: true, pointer, reasonCodes: gate.reasonCodes };\n}\n\n/** Roll back to a previous policy. Idempotent; old policy retained. */\nexport function rollbackPolicy(policyId: string, authorizedBy: string): { ok: boolean; error?: string } {\n\tconst policy = readPolicy(policyId);\n\tif (!policy) return { ok: false, error: `policy ${policyId} not found` };\n\n\tconst pointer = {\n\t\tpolicyId: policy.policyId,\n\t\tpolicyVersion: policy.policyVersion,\n\t\thash: policy.hash,\n\t\tactivatedAt: new Date().toISOString(),\n\t};\n\tsaveActivePolicyPointer(pointer);\n\tappendEvent({\n\t\ttype: \"ROUTING_POLICY_ROLLED_BACK\",\n\t\tpolicyId: policy.policyId,\n\t\tpolicyVersion: policy.policyVersion,\n\t\tpayload: { authorizedBy },\n\t});\n\treturn { ok: true };\n}\n\n/** Compare two policies deterministically. */\nexport function comparePolicies(a: RoutingPolicyCandidate, b: RoutingPolicyCandidate): unknown {\n\treturn {\n\t\tleft: { policyId: a.policyId, version: a.policyVersion, preferences: a.preferences },\n\t\tright: { policyId: b.policyId, version: b.policyVersion, preferences: b.preferences },\n\t\tsame: stableStringify(a.preferences) === stableStringify(b.preferences),\n\t};\n}\n"]}