{"version":3,"file":"transition-policy.d.ts","sourceRoot":"","sources":["../../../src/core/long-horizon/transition-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AAEzE,OAAO,KAAK,EAIX,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,MAAM,YAAY,CAAC;AAsCpB,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,kBAAkB,CACjC,aAAa,EAAE,2BAA2B,EAC1C,OAAO,EAAE,iBAAiB,GACxB,sBAAsB,CAqCxB;AAMD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAClC,cAAc,EAAE,2BAA2B,EAC3C,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,4BAA4B,GACnC,sBAAsB,CAyBxB;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,4BAA4B,GACnC,sBAAsB,CA8FxB;AAMD;;;;GAIG;AACH,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,mBAAmB,EAC3B,QAAQ,EAAE,iBAAiB,EAC3B,wBAAwB,EAAE,MAAM,GAAG,SAAS,GAC1C,sBAAsB,CA6BxB;AAoGD;;GAEG;AACH,wBAAgB,uBAAuB,CACtC,aAAa,EAAE,2BAA2B,GACxC,WAAW,CAAC,2BAA2B,CAAC,CAE1C;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,2BAA2B,GAAG,OAAO,CAE9F","sourcesContent":["/**\n * Canonical Transition Policy for Requirement Ledger v1.\n *\n * One single source of truth for all permitted state transitions.\n * No transition rule may be duplicated across this module, the CLI,\n * or any validator — all paths consult this policy.\n *\n * Authorization is now derived from the TrustedLedgerMutationContext,\n * not from serialized actor strings in the request payload.\n */\n\nimport type { RequirementEvaluationStatus } from \"./domain-types.js\";\nimport type { TrustedLedgerMutationContext } from \"./trusted-context.js\";\nimport { contextHasCapability } from \"./trusted-context.js\";\nimport type {\n\tAcceptanceCriterion,\n\tEvidenceRequirement,\n\tLedgerEvidenceRecord,\n\tMissionContractV1,\n\tRequirementLedgerV1,\n\tTransitionRequest,\n} from \"./types.js\";\n\n// =============================================================================\n// Transition matrix — permitted transitions\n// =============================================================================\n\n/**\n * Map from current state to set of allowed next states.\n * Any transition not listed here is FORBIDDEN.\n */\nconst PERMITTED_TRANSITIONS: Record<RequirementEvaluationStatus, Set<RequirementEvaluationStatus>> = {\n\tUNASSESSED: new Set([\"PENDING\", \"IN_PROGRESS\", \"IMPLEMENTED_UNVERIFIED\", \"NOT_APPLICABLE\"]),\n\tPENDING: new Set([\"IN_PROGRESS\", \"IMPLEMENTED_UNVERIFIED\", \"BLOCKED\", \"NOT_APPLICABLE\"]),\n\tIN_PROGRESS: new Set([\"IMPLEMENTED_UNVERIFIED\", \"BLOCKED\", \"FAILED\"]),\n\tIMPLEMENTED_UNVERIFIED: new Set([\"SATISFIED\", \"IN_PROGRESS\", \"FAILED\", \"BLOCKED\"]),\n\tSATISFIED: new Set([\"IMPLEMENTED_UNVERIFIED\", \"IN_PROGRESS\", \"FAILED\"]),\n\tBLOCKED: new Set([\"IN_PROGRESS\", \"PENDING\", \"FAILED\", \"NOT_APPLICABLE\"]),\n\tNOT_APPLICABLE: new Set([\"PENDING\", \"UNASSESSED\"]),\n\tFAILED: new Set([\"IN_PROGRESS\", \"PENDING\", \"BLOCKED\"]),\n};\n\n// =============================================================================\n// Forbidden completion paths\n// =============================================================================\n\n/** States that may NEVER transition directly to SATISFIED. */\nconst FORBIDDEN_DIRECT_SATISFIED: Set<RequirementEvaluationStatus> = new Set([\n\t\"UNASSESSED\",\n\t\"PENDING\",\n\t\"IN_PROGRESS\",\n\t\"BLOCKED\",\n\t\"FAILED\",\n]);\n\n// =============================================================================\n// Transition validation result\n// =============================================================================\n\nexport interface TransitionPolicyResult {\n\tpermitted: boolean;\n\treason?: string;\n}\n\n// =============================================================================\n// Validate a proposed transition (structural only, no auth)\n// =============================================================================\n\nexport function validateTransition(\n\tcurrentStatus: RequirementEvaluationStatus,\n\trequest: TransitionRequest,\n): TransitionPolicyResult {\n\tconst toStatus = request.toStatus;\n\n\t// 1. Structural: is the transition in the matrix?\n\tconst allowed = PERMITTED_TRANSITIONS[currentStatus];\n\tif (!allowed?.has(toStatus)) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: `Transition from ${currentStatus} to ${toStatus} is not permitted`,\n\t\t};\n\t}\n\n\t// 2. Completion boundary: SATISFIED only from IMPLEMENTED_UNVERIFIED\n\tif (toStatus === \"SATISFIED\" && currentStatus !== \"IMPLEMENTED_UNVERIFIED\") {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: `SATISFIED requires prior IMPLEMENTED_UNVERIFIED state, got ${currentStatus}`,\n\t\t};\n\t}\n\n\t// 3. BLOCKED requires a reason\n\tif (toStatus === \"BLOCKED\" && !request.blockerReference && !request.reason) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: \"Transition to BLOCKED requires a blocker reference or reason\",\n\t\t};\n\t}\n\n\t// 4. FAILED requires a reason\n\tif (toStatus === \"FAILED\" && !request.reason) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: \"Transition to FAILED requires a reason\",\n\t\t};\n\t}\n\n\treturn { permitted: true };\n}\n\n// =============================================================================\n// Authorize a transition using trusted context (replaces actorType checks)\n// =============================================================================\n\n/**\n * Check if the transition is authorized given the trusted context.\n *\n * This replaces the old `actorType` field check.\n * Authorization is derived from the opaque trusted context, not from\n * serialized payload fields.\n */\nexport function authorizeTransition(\n\t_currentStatus: RequirementEvaluationStatus,\n\trequest: TransitionRequest,\n\tcontext: TrustedLedgerMutationContext,\n): TransitionPolicyResult {\n\tconst toStatus = request.toStatus;\n\n\t// SATISFIED: requires transition:satisfy capability\n\tif (toStatus === \"SATISFIED\") {\n\t\tif (!contextHasCapability(context, \"transition:satisfy\")) {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: \"SATISFIED transition requires transition:satisfy capability; untrusted context lacks it\",\n\t\t\t};\n\t\t}\n\t}\n\n\t// Runtime NOT_APPLICABLE: requires transition:not-applicable capability\n\t// (Initial contract NOT_APPLICABLE does not go through this path)\n\tif (toStatus === \"NOT_APPLICABLE\") {\n\t\tif (!contextHasCapability(context, \"transition:not-applicable\")) {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: \"Runtime NOT_APPLICABLE requires transition:not-applicable capability; untrusted context lacks it\",\n\t\t\t};\n\t\t}\n\t}\n\n\treturn { permitted: true };\n}\n\n// =============================================================================\n// SATISFIED boundary enforcement (criterion-level)\n// =============================================================================\n\n/**\n * Check if the satisfaction boundary is violated.\n * Called when a transition to SATISFIED is attempted.\n *\n * This enforces EVERY acceptance criterion defined in the contract\n * for the target requirement using ONLY the evidence IDs explicitly\n * referenced in the transition request (request.evidenceIds).\n *\n * Unreferenced evidence elsewhere in the ledger CANNOT authorize\n * the transition. This ensures mutation-time authorization and\n * historical replay use the same canonical evidence set.\n *\n * Authorization is derived from context, not from actorType.\n */\nexport function isSatisfactionAuthorized(\n\trequest: TransitionRequest,\n\tledger: RequirementLedgerV1,\n\tcontract: MissionContractV1,\n\tcontext: TrustedLedgerMutationContext,\n): TransitionPolicyResult {\n\t// Must have transition:satisfy capability\n\tif (!contextHasCapability(context, \"transition:satisfy\")) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: \"Agent cannot self-authorize SATISFIED; authoritative evidence from a trusted source required\",\n\t\t};\n\t}\n\n\t// Evidence must be present\n\tif (request.evidenceIds.length === 0) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: \"SATISFIED requires at least one evidence reference\",\n\t\t};\n\t}\n\n\t// Reject duplicate evidence IDs in the request\n\tconst seenEvidenceIds = new Set<string>();\n\tfor (const evId of request.evidenceIds) {\n\t\tif (seenEvidenceIds.has(evId)) {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: `Duplicate evidence id in transition request: ${evId}`,\n\t\t\t};\n\t\t}\n\t\tseenEvidenceIds.add(evId);\n\t}\n\n\t// Resolve all evidence records from ONLY the request's evidenceIds\n\tconst resolvedEvidence: LedgerEvidenceRecord[] = [];\n\tfor (const evId of request.evidenceIds) {\n\t\tconst evRecord = ledger.evidence.find((e) => e.id === evId);\n\t\tif (!evRecord) {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: `Evidence ${evId} not found in ledger`,\n\t\t\t};\n\t\t}\n\t\tif (evRecord.effectiveAuthority === \"agent-claim\") {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: `Evidence ${evId} is an agent claim — non-authoritative`,\n\t\t\t};\n\t\t}\n\t\tif (evRecord.status !== \"pass\") {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: `Evidence ${evId} has status \"${evRecord.status}\" — must be \"pass\"`,\n\t\t\t};\n\t\t}\n\t\t// Evidence must bind to the target requirement\n\t\tif (!evRecord.requirementIds.includes(request.requirementId)) {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: `Evidence ${evId} does not reference requirement ${request.requirementId}`,\n\t\t\t};\n\t\t}\n\t\t// Evidence must exist before the transition revision\n\t\t// (checked at a higher level in applyRequirementTransition)\n\t\tresolvedEvidence.push(evRecord);\n\t}\n\n\t// =========================================================================\n\t// CRITERION-LEVEL ENFORCEMENT\n\t// Every acceptance criterion for this requirement must be independently\n\t// satisfied by at least one piece of evidence named in request.evidenceIds.\n\t// =========================================================================\n\n\tconst requirement = contract.requirements.find((r) => r.id === request.requirementId);\n\tif (!requirement) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: `Requirement ${request.requirementId} not found in contract`,\n\t\t};\n\t}\n\n\tif (requirement.acceptanceCriteria.length === 0) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: `Requirement ${request.requirementId} has no acceptance criteria`,\n\t\t};\n\t}\n\n\t// Evaluate each criterion using ONLY the resolved evidence set (same as\n\t// what will be persisted in transition.evidenceIds).\n\tfor (const criterion of requirement.acceptanceCriteria) {\n\t\tconst satisfied = isCriterionSatisfied(criterion, resolvedEvidence, request.requirementId);\n\t\tif (!satisfied.permitted) {\n\t\t\treturn satisfied;\n\t\t}\n\t}\n\n\treturn { permitted: true };\n}\n\n// =============================================================================\n// Evidence freshness after regression check\n// =============================================================================\n\n/**\n * When a requirement exits SATISFIED through regression, record the regression\n * revision. A later transition back to SATISFIED must use evidence where every\n * criterion has at least one piece of evidence added AFTER the regression.\n */\nexport function checkEvidenceFreshnessAfterRegression(\n\trequest: TransitionRequest,\n\tledger: RequirementLedgerV1,\n\tcontract: MissionContractV1,\n\tlatestRegressionRevision: number | undefined,\n): TransitionPolicyResult {\n\tif (request.toStatus !== \"SATISFIED\") return { permitted: true };\n\tif (latestRegressionRevision === undefined) return { permitted: true };\n\n\tconst requirement = contract.requirements.find((r) => r.id === request.requirementId);\n\tif (!requirement) return { permitted: true };\n\n\t// For each acceptance criterion, check if at least one referenced evidence\n\t// was added AFTER the regression revision.\n\tfor (const criterion of requirement.acceptanceCriteria) {\n\t\tconst criterionEvidenceIds = request.evidenceIds.filter((evId) => {\n\t\t\tconst ev = ledger.evidence.find((e) => e.id === evId);\n\t\t\treturn ev?.criterionIds.includes(criterion.id);\n\t\t});\n\n\t\tconst hasFresh = criterionEvidenceIds.some((evId) => {\n\t\t\tconst ev = ledger.evidence.find((e) => e.id === evId);\n\t\t\treturn ev && ev.addedAtRevision > latestRegressionRevision;\n\t\t});\n\n\t\tif (!hasFresh) {\n\t\t\treturn {\n\t\t\t\tpermitted: false,\n\t\t\t\treason: `Criterion \"${criterion.id}\": no evidence added after regression at revision ${latestRegressionRevision}`,\n\t\t\t};\n\t\t}\n\t}\n\n\treturn { permitted: true };\n}\n\n/**\n * Verify that an acceptance criterion is satisfied by at least one\n * evidence record in the ledger that fulfills every required-evidence\n * constraint.\n */\nfunction isCriterionSatisfied(\n\tcriterion: AcceptanceCriterion,\n\tevidence: LedgerEvidenceRecord[],\n\trequirementId: string,\n): TransitionPolicyResult {\n\tconst matchingCriterionEvidence = evidence.filter(\n\t\t(ev) => ev.requirementIds.includes(requirementId) && ev.criterionIds.includes(criterion.id),\n\t);\n\n\tif (matchingCriterionEvidence.length === 0) {\n\t\treturn {\n\t\t\tpermitted: false,\n\t\t\treason: `Criterion \"${criterion.id}\": no evidence in ledger`,\n\t\t};\n\t}\n\n\t// Find at least one evidence record that meets ALL evidence requirements for this criterion\n\tfor (const evRecord of matchingCriterionEvidence) {\n\t\tconst meetsAll = criterion.requiredEvidence.every((req) => evidenceMeetsRequirement(evRecord, req));\n\n\t\tif (meetsAll) {\n\t\t\treturn { permitted: true };\n\t\t}\n\t}\n\n\treturn {\n\t\tpermitted: false,\n\t\treason: `Criterion \"${criterion.id}\": no evidence satisfies all required evidence constraints`,\n\t};\n}\n\n/**\n * Check if a single evidence record meets a single EvidenceRequirement.\n */\nfunction evidenceMeetsRequirement(evRecord: LedgerEvidenceRecord, requirement: EvidenceRequirement): boolean {\n\t// Evidence must have passing status\n\tif (evRecord.status !== \"pass\") {\n\t\treturn false;\n\t}\n\n\t// Agent claims are never authoritative\n\tif (evRecord.effectiveAuthority === \"agent-claim\") {\n\t\treturn false;\n\t}\n\n\t// allowedTypes: evidence type must be in the allowed set\n\tif (requirement.allowedTypes && requirement.allowedTypes.length > 0) {\n\t\tif (!requirement.allowedTypes.includes(evRecord.type)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// minAuthority: effective authority must meet the minimum\n\tif (requirement.minAuthority) {\n\t\tif (!authorityMeetsMinimum(evRecord.effectiveAuthority, requirement.minAuthority)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// requiredCollectorClass: check verified principal kind (not collectorType string)\n\tif (requirement.requiredCollectorClass) {\n\t\tif (evRecord.verifiedPrincipalKind !== requirement.requiredCollectorClass) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n/**\n * Authority hierarchy: whether `actual` meets or exceeds `minimum`.\n *\n * Hierarchy (lowest to highest):\n *   agent-claim < repository-observation < command-result < test-result\n *   < runtime-observation < operator-confirmation < trusted-collector\n */\nfunction authorityMeetsMinimum(actual: string, minimum: string): boolean {\n\tconst ranks: Record<string, number> = {\n\t\t\"agent-claim\": 0,\n\t\t\"repository-observation\": 1,\n\t\t\"command-result\": 2,\n\t\t\"test-result\": 3,\n\t\t\"runtime-observation\": 4,\n\t\t\"operator-confirmation\": 5,\n\t\t\"trusted-collector\": 6,\n\t};\n\n\tconst actualRank = ranks[actual] ?? -1;\n\tconst minRank = ranks[minimum] ?? -1;\n\n\treturn actualRank >= minRank;\n}\n\n/**\n * Get all permitted next states from a given state.\n */\nexport function getPermittedTransitions(\n\tcurrentStatus: RequirementEvaluationStatus,\n): ReadonlySet<RequirementEvaluationStatus> {\n\treturn PERMITTED_TRANSITIONS[currentStatus] ?? new Set();\n}\n\n/**\n * Check if a direct transition to SATISFIED is forbidden from a given state.\n */\nexport function isForbiddenDirectSatisfied(currentStatus: RequirementEvaluationStatus): boolean {\n\treturn FORBIDDEN_DIRECT_SATISFIED.has(currentStatus);\n}\n"]}