import { isBugbotFreshnessDiagnostic, type BugbotFreshnessDiagnostic } from '../../domain/bugbot/review_freshness_failure'; import { isBugbotPartitionQueryDiagnostic, type BugbotPartitionQueryDiagnostic } from '../../domain/bugbot/response_failure'; import { isProgressBatchDiagnostic, type ProgressBatchDiagnostic } from '../../domain/progress_batch_diagnostic'; import { isBranchSyncPublicationDiagnostic, type BranchSyncPublicationDiagnostic } from '../../domain/branch_sync_publication'; import { isBugbotReviewBlockFacts, type BugbotReviewBlockFacts } from '../../domain/bugbot/framed_review'; import { isProgressScopeCapacityFacts, type ProgressScopeCapacityFacts } from '../../domain/progress_response_diagnostic'; import { normalizeProgressCompletionDiagnostic, type ProgressCompletionDiagnostic } from '../../domain/progress_completion_diagnostic'; import { isProgressInventoryDiagnostic, type ProgressInventoryDiagnostic } from '../../domain/progress_inventory_diagnostic'; import { normalizeProgressCapacityDiagnostic, type ProgressCapacityDiagnostic } from '../../domain/progress_capacity_diagnostic'; import { isProgressResponseDiagnosticFacts, type ProgressResponseDiagnosticFacts } from '../../domain/progress_response_diagnostic'; import { isProgressPartitionLengthDiagnostic, isProgressPartitionByteDiagnostic, type ProgressPartitionLengthDiagnostic, type ProgressPartitionByteDiagnostic } from '../../domain/progress_partition_length_failure'; import { isProgressAssessmentLengthDiagnostic, type ProgressAssessmentLengthDiagnostic } from '../../domain/progress_assessment_length_failure'; import { isProgressBinaryEvidenceFacts, type ProgressBinaryEvidenceFacts } from '../../domain/progress_binary_evidence_diagnostic'; import { isIssueWorkflowDiagnostic, type IssueWorkflowDiagnosticFacts } from '../../domain/issue_workflow_diagnostic'; import { isChangeSizeReconciliationFacts, type ChangeSizeReconciliationFacts } from '../../domain/change_size_reconciliation'; import { isIssueAssignmentDiagnostic, type IssueAssignmentDiagnostic } from '../../domain/issue_assignment_diagnostic'; import { COPILOT_COMMAND_VALIDATION_REASONS, isCopilotCommandValidationReason, isValidCopilotCommandValidation, type CopilotCommandValidationReason } from '../../domain/copilot_command_validation'; import type { CopilotCommandName } from '../../domain/copilot_command'; import { CLOSED_ISSUE_RESULTS, CLOSED_ISSUE_PHASES, CLOSED_ISSUE_REASONS, type ClosedIssueReconciliationFacts } from '../../domain/closed_issue_reconciliation'; import { APPROVAL_EVIDENCE_PERMISSIONS, APPROVAL_EVIDENCE_REASONS, type ApprovalEvidenceDiagnostic } from '../../domain/approval_evidence_diagnostic'; import { BRANCH_SYNC_VALIDATION_CODES, type BranchSyncValidationCode } from '../../domain/branch_sync_validation'; import { isBranchSyncVerificationFailure, type BranchSyncVerificationFailure } from '../../domain/verification_failure'; import { isBranchSyncCatalogDiagnostic, type BranchSyncCatalogDiagnostic } from '../../domain/branch_sync_catalog_diagnostic'; import { isSddSourceContextDiagnostic, type SddSourceContextDiagnostic } from '../../domain/agent_source_snapshot'; import { isSddDraftMinimumDiagnostic, type SddDraftMinimumDiagnostic } from '../../domain/sdd_validation_diagnostic'; export type ApplicationErrorKind = | 'configuration' | 'authorization' | 'provider' | 'agent' | 'validation' | 'workflow' | 'unknown'; export type ApplicationErrorCode = | 'configuration.invalid' | 'configuration.unsupported' | 'authorization.denied' | 'authorization.credential-invalid' | 'provider.not-found' | 'provider.conflict' | 'provider.rate-limited' | 'provider.unavailable' | 'provider.contract-invalid' | 'agent.policy-rejected' | 'agent.failed' | 'locale.output-invalid' | 'locale.translation-failed' | 'validation.invalid-input' | 'validation.evidence-too-large' | 'validation.evidence-unavailable' | 'workflow.invalid-event' | 'workflow.stale' | 'workflow.cancelled' | 'workflow.failed' | 'workflow.presentation-pending' | 'timeout' | 'unexpected'; export const APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ ...COPILOT_COMMAND_VALIDATION_REASONS, 'pull-request-link-restored', 'pull-request-link-base-retained', 'pull-request-link-reference-retained', 'pull-request-link-base-and-reference-retained', 'managed-branch-enrichment-failed', 'inactivity-explanation-failed', 'bugbot-review-blocks-pending', 'branch-sync-agent-disabled', 'branch-sync-fixer-unavailable', 'branch-sync-sensitive-paths', 'branch-sync-conflict-limit', 'branch-sync-resolution-invalid', 'branch-sync-verification-failed', 'branch-sync-catalog-incompatible', 'branch-sync-workflow-write-required', 'branch-sync-publication-failed', 'sdd-draft-size-exceeded', 'sdd-draft-response-too-short', 'sdd-catalog-unsupported-profile', 'sdd-catalog-format-invalid', 'sdd-catalog-missing-trusted-source', 'sdd-source-context-unavailable', 'sdd-draft-format-invalid', 'sdd-draft-case-identity-invalid', 'sdd-draft-case-budget-invalid', 'sdd-draft-requirement-mapping-invalid', 'sdd-draft-repository-path-invalid', 'deployment-publication-context-missing', 'workspace-changes-rejected', 'closed-issue-reconciliation-failed', 'approval-evidence-read-failed', 'change-size-reconciliation-failed', 'issue-assignment-failed', 'issue-assignment-count-invalid', 'issue-workflow-admission-blocked', 'progress-binary-evidence-unavailable', 'progress-inventory-incomplete', 'progress-evidence-capacity-exceeded', 'bugbot-review-incomplete', 'progress-result-contract-invalid', 'progress-partition-length-invalid', 'progress-partition-bytes-invalid', 'progress-assessment-length-invalid', 'bugbot-partition-query-exhausted', 'bugbot-freshness-before-publication', 'bugbot-freshness-after-publication', 'progress-assessment-capacity-exceeded', 'progress-scope-clause-limit', 'progress-sdd-seed-unverified', 'progress-batch-retained', 'progress-completion-recovery-exhausted', 'stored-configuration-schema-missing', 'stored-configuration-schema-unsupported', 'stored-configuration-invalid', ] as const); export type ApplicationErrorRecoveryId = typeof APPLICATION_ERROR_RECOVERY_IDS[number]; interface ApplicationErrorRecoveryVariables extends Readonly>> { readonly 'stored-configuration-schema-missing': Readonly<{ issueNumber: number; expectedVersion: number }>; readonly 'stored-configuration-schema-unsupported': Readonly<{ issueNumber: number; expectedVersion: number; actualVersion: number }>; readonly 'stored-configuration-invalid': Readonly<{ issueNumber: number; expectedVersion: number }>; readonly 'closed-issue-reconciliation-failed': ClosedIssueReconciliationFacts; readonly 'approval-evidence-read-failed': ApprovalEvidenceDiagnostic; readonly 'change-size-reconciliation-failed': ChangeSizeReconciliationFacts; readonly 'issue-assignment-failed': IssueAssignmentDiagnostic; readonly 'issue-assignment-count-invalid': Readonly>; readonly 'issue-workflow-admission-blocked': IssueWorkflowDiagnosticFacts; readonly 'progress-binary-evidence-unavailable': ProgressBinaryEvidenceFacts; readonly 'progress-inventory-incomplete': ProgressInventoryDiagnostic; readonly 'progress-evidence-capacity-exceeded': ProgressCapacityDiagnostic; readonly 'bugbot-review-incomplete': BugbotReviewBlockFacts; readonly 'progress-result-contract-invalid': ProgressResponseDiagnosticFacts; readonly 'progress-partition-length-invalid': ProgressPartitionLengthDiagnostic; readonly 'progress-partition-bytes-invalid': ProgressPartitionByteDiagnostic; readonly 'progress-assessment-length-invalid': ProgressAssessmentLengthDiagnostic; readonly 'bugbot-partition-query-exhausted': BugbotPartitionQueryDiagnostic; readonly 'bugbot-freshness-before-publication': BugbotFreshnessDiagnostic; readonly 'bugbot-freshness-after-publication': BugbotFreshnessDiagnostic; readonly 'progress-assessment-capacity-exceeded': ProgressResponseDiagnosticFacts; readonly 'progress-sdd-seed-unverified': Readonly>; readonly 'progress-batch-retained': ProgressBatchDiagnostic; readonly 'progress-scope-clause-limit': ProgressScopeCapacityFacts; readonly 'progress-completion-recovery-exhausted': ProgressCompletionDiagnostic; readonly 'pull-request-link-restored': Readonly>; readonly 'pull-request-link-base-retained': Readonly>; readonly 'pull-request-link-reference-retained': Readonly>; readonly 'pull-request-link-base-and-reference-retained': Readonly>; readonly 'managed-branch-enrichment-failed': Readonly<{ branchName: string }>; readonly 'inactivity-explanation-failed': Readonly<{ issueNumber: number }>; readonly 'bugbot-review-blocks-pending': Readonly<{ pendingCount: number }>; readonly 'branch-sync-agent-disabled': Readonly<{ command: '/copilot sync-branch' }>; readonly 'branch-sync-fixer-unavailable': Readonly<{ command: '/copilot sync-branch' }>; readonly 'branch-sync-sensitive-paths': Readonly<{ command: '/copilot sync-branch'; conflictCount: number; sensitiveCount: number }>; readonly 'branch-sync-conflict-limit': Readonly<{ command: '/copilot sync-branch'; conflictCount: number; limit: 20 }>; readonly 'branch-sync-resolution-invalid': Readonly<{ validationCode: BranchSyncValidationCode; command: '/copilot sync-branch' }>; readonly 'branch-sync-verification-failed': BranchSyncVerificationFailure; readonly 'branch-sync-catalog-incompatible': BranchSyncCatalogDiagnostic & Readonly<{ abort: 'completed' | 'unverified' }>; readonly 'branch-sync-workflow-write-required': BranchSyncPublicationDiagnostic; readonly 'branch-sync-publication-failed': BranchSyncPublicationDiagnostic; readonly 'sdd-draft-size-exceeded': Readonly>; readonly 'sdd-draft-response-too-short': SddDraftMinimumDiagnostic; readonly 'sdd-catalog-unsupported-profile': Readonly>; readonly 'sdd-catalog-format-invalid': Readonly>; readonly 'sdd-catalog-missing-trusted-source': Readonly>; readonly 'sdd-source-context-unavailable': SddSourceContextDiagnostic; readonly 'sdd-draft-format-invalid': Readonly>; readonly 'sdd-draft-case-identity-invalid': Readonly>; readonly 'sdd-draft-case-budget-invalid': Readonly>; readonly 'sdd-draft-requirement-mapping-invalid': Readonly>; readonly 'sdd-draft-repository-path-invalid': Readonly>; readonly 'deployment-publication-context-missing': Readonly>; readonly 'workspace-changes-rejected': Readonly>; } export type ApplicationErrorRecovery = { readonly [Id in ApplicationErrorRecoveryId]: Readonly<{ id: Id; variables: ApplicationErrorRecoveryVariables[Id]; }>; }[ApplicationErrorRecoveryId]; interface ApplicationErrorMetadata { readonly kind: ApplicationErrorKind; readonly retryable: boolean; readonly impact: string; readonly action: string; readonly retainedState: string; } const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; export const APPLICATION_ERROR_METADATA: Readonly> = { 'configuration.invalid': { kind: 'configuration', retryable: false, impact: 'The operation could not use the configured values.', action: 'Correct the invalid configuration and retry.', retainedState: UNCHANGED_STATE, }, 'configuration.unsupported': { kind: 'configuration', retryable: false, impact: 'The requested capability is not supported by this installation.', action: 'Use a supported configuration or update the installation.', retainedState: UNCHANGED_STATE, }, 'authorization.denied': { kind: 'authorization', retryable: false, impact: 'The operation could not access the required resource.', action: 'Grant the documented permission and retry.', retainedState: UNCHANGED_STATE, }, 'authorization.credential-invalid': { kind: 'authorization', retryable: false, impact: 'The operation could not authenticate with the required provider.', action: 'Replace or configure the required credential and retry.', retainedState: UNCHANGED_STATE, }, 'provider.not-found': { kind: 'provider', retryable: false, impact: 'A required provider resource was not found.', action: 'Verify the target resource and retry the operation.', retainedState: PRESERVED_STATE, }, 'provider.conflict': { kind: 'provider', retryable: true, impact: 'The provider rejected a conflicting current state.', action: 'Reload the current state and retry if the operation is still required.', retainedState: PRESERVED_STATE, }, 'provider.rate-limited': { kind: 'provider', retryable: true, impact: 'The provider temporarily limited the operation.', action: 'Retry after the provider limit resets.', retainedState: PRESERVED_STATE, }, 'provider.unavailable': { kind: 'provider', retryable: true, impact: 'The provider was temporarily unavailable.', action: 'Retry when the provider is available.', retainedState: PRESERVED_STATE, }, 'provider.contract-invalid': { kind: 'provider', retryable: false, impact: 'The provider response could not be safely interpreted.', action: 'Review the provider integration before retrying.', retainedState: PRESERVED_STATE, }, 'agent.policy-rejected': { kind: 'agent', retryable: false, impact: 'The configured agent was not started.', action: 'Use an allowed agent configuration and retry.', retainedState: UNCHANGED_STATE, }, 'agent.failed': { kind: 'agent', retryable: true, impact: 'The admitted agent did not produce a usable result.', action: 'Inspect the sanitized agent status and retry if appropriate.', retainedState: PRESERVED_STATE, }, 'locale.output-invalid': { kind: 'agent', retryable: true, impact: 'Agent-generated product content was rejected before publication because its locale contract was invalid.', action: 'Retry with a provider that supports the configured repository locale.', retainedState: UNCHANGED_STATE, }, 'locale.translation-failed': { kind: 'agent', retryable: true, impact: 'The request could not be safely interpreted in the configured repository language.', action: 'Rephrase the request or retry when the configured language provider is available.', retainedState: UNCHANGED_STATE, }, 'validation.invalid-input': { kind: 'validation', retryable: false, impact: 'The operation did not accept the supplied input.', action: 'Correct the input and retry.', retainedState: UNCHANGED_STATE, }, 'validation.evidence-unavailable': { kind: 'validation', retryable: false, impact: 'Progress was not assessed because a required patch and its complete immutable contents were unavailable.', action: 'Inspect the unreviewed paths and intended pull request base in the exact base/head comparison. Provide complete reviewable evidence before a new assessment. Retrying the same commits cannot restore unavailable immutable evidence.', retainedState: 'Existing progress labels and published assessments were preserved; no model assessment was made.', }, 'validation.evidence-too-large': { kind: 'validation', retryable: false, impact: 'Progress could not be assessed within the fixed evidence limits.', action: 'Inspect the complete comparison and available capacity facts. Bounded multipart continuation is invocation-local; this capacity failure stops the attempt. Retrying unchanged over-limit evidence cannot increase capacity. Preserve complete acceptance and evidence, and report unsupported workloads with the run link.', retainedState: 'Existing progress labels and published assessments were preserved.', }, 'workflow.invalid-event': { kind: 'workflow', retryable: false, impact: 'The event cannot start the requested workflow.', action: 'Start the operation from a supported event or surface.', retainedState: UNCHANGED_STATE, }, 'workflow.stale': { kind: 'workflow', retryable: false, impact: 'A newer state superseded this workflow invocation.', action: 'Inspect the current state and start a fresh invocation only if needed.', retainedState: PRESERVED_STATE, }, 'workflow.cancelled': { kind: 'workflow', retryable: false, impact: 'The workflow stopped before it completed.', action: 'Start a new invocation if the operation is still required.', retainedState: PRESERVED_STATE, }, 'workflow.failed': { kind: 'workflow', retryable: true, impact: 'The workflow could not complete the requested operation.', action: 'Inspect the current state and retry the failed step.', retainedState: PRESERVED_STATE, }, 'workflow.presentation-pending': { kind: 'workflow', retryable: true, impact: 'Bugbot completed the review, but historical review summaries are not fully synchronized.', action: 'Run a Bugbot recheck to continue the bounded presentation repair.', retainedState: PRESERVED_STATE, }, timeout: { kind: 'workflow', retryable: true, impact: 'The operation exceeded its bounded execution time.', action: 'Verify the current state before retrying.', retainedState: PRESERVED_STATE, }, unexpected: { kind: 'unknown', retryable: false, impact: 'The operation stopped because an unexpected failure was handled safely.', action: 'Use the correlation ID to investigate before retrying.', retainedState: PRESERVED_STATE, }, }; export interface SemanticApplicationErrorOptions { readonly retryable?: boolean; readonly recovery?: ApplicationErrorRecovery; readonly correlationId: string; readonly cause?: unknown; } export interface ApplicationErrorPublicRecord { readonly name: 'ApplicationError'; readonly message: string; readonly code: ApplicationErrorCode; readonly kind: ApplicationErrorKind; readonly retryable: boolean; readonly impact: string; readonly action: string; readonly retainedState: string; readonly correlationId: string; readonly recovery?: ApplicationErrorRecovery; } const CORRELATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; export function isApplicationErrorCorrelationId(value: string): boolean { return CORRELATION_ID_PATTERN.test(value); } /** Semantic error contract whose public fields are safe to serialize and present. */ export class ApplicationError extends Error { override readonly name: 'ApplicationError'; readonly code: ApplicationErrorCode; readonly kind: ApplicationErrorKind; readonly retryable: boolean; readonly impact: string; readonly action: string; readonly retainedState: string; readonly correlationId: string; readonly recovery?: ApplicationErrorRecovery; // The cause is intentionally debugger-only: no accessor or serializer may expose it. // eslint-disable-next-line no-unused-private-class-members readonly #cause?: unknown; constructor(code: ApplicationErrorCode, message: string, options: SemanticApplicationErrorOptions) { super(message); const metadata = APPLICATION_ERROR_METADATA[code]; const correlationId = options.correlationId; if (!isApplicationErrorCorrelationId(correlationId)) { throw new TypeError('Application error correlation ID must be a lowercase UUID v4.'); } if (options.retryable === true && !metadata.retryable) { throw new TypeError(`Retryability cannot be broadened for ${code}.`); } this.name = 'ApplicationError'; this.code = code; this.kind = metadata.kind; this.retryable = options.retryable ?? metadata.retryable; this.impact = metadata.impact; this.action = metadata.action; this.retainedState = options.recovery?.id === 'change-size-reconciliation-failed' ? 'Completed effects were retained; the failed and later size operations remain unverified.' : options.recovery?.id === 'issue-assignment-failed' ? 'Verified assignments and independently completed effects were retained; the failed assignment operation remains unverified.' : options.recovery?.id === 'issue-assignment-count-invalid' ? 'No assignment operation was attempted; independently completed lifecycle effects were retained.' : options.recovery?.id === 'sdd-draft-response-too-short' ? 'Existing answers, selected ownership and published specifications were retained; this attempt published no specification or branch.' : options.recovery?.id === 'progress-evidence-capacity-exceeded' ? 'Existing progress labels and assessments were preserved. No partial progress was published; independent completed workflow effects remain in their own results.' : options.recovery?.id === 'deployment-publication-context-missing' ? PRESERVED_STATE : metadata.retainedState; this.correlationId = correlationId; this.recovery = normalizeApplicationErrorRecovery(options.recovery); this.#cause = options.cause; } toJSON(): ApplicationErrorPublicRecord { return { name: 'ApplicationError', message: this.message, code: this.code, kind: this.kind, retryable: this.retryable, impact: this.impact, action: this.action, retainedState: this.retainedState, correlationId: this.correlationId, ...(this.recovery ? { recovery: this.recovery } : {}), }; } } const RECOVERY_VARIABLE_KEYS: Readonly> = Object.freeze({ 'comment-command-length': Object.freeze(['command']), 'comment-command-name-missing': Object.freeze(['command']), 'comment-command-name-unknown': Object.freeze(['command']), 'comment-command-argument-count': Object.freeze(['command']), 'comment-command-arguments-unexpected': Object.freeze(['command']), 'comment-command-arguments-missing': Object.freeze(['command']), 'comment-command-finding-quoting': Object.freeze(['command']), 'comment-command-review-format': Object.freeze(['command']), 'comment-command-review-effort': Object.freeze(['command']), 'comment-command-review-boolean': Object.freeze(['command']), 'comment-command-review-option': Object.freeze(['command']), 'comment-command-sync-option': Object.freeze(['command']), 'comment-command-sync-parent': Object.freeze(['command']), 'stored-configuration-schema-missing': Object.freeze(['issueNumber', 'expectedVersion']), 'stored-configuration-schema-unsupported': Object.freeze(['issueNumber', 'expectedVersion', 'actualVersion']), 'stored-configuration-invalid': Object.freeze(['issueNumber', 'expectedVersion']), 'closed-issue-reconciliation-failed': Object.freeze(['resultId', 'phase', 'state', 'reason', 'changed', 'remaining']), 'approval-evidence-read-failed': Object.freeze(['operation', 'reason', 'permission']), 'issue-workflow-admission-blocked': Object.freeze(['kind', 'reason', 'fields']), 'change-size-reconciliation-failed': Object.freeze(['operation', 'issueNumber', 'targetNumber', 'projectIndex', 'verifiedOperations', 'pendingProjectSizes', 'providerStatus', 'providerReference']), 'issue-assignment-failed': Object.freeze(['targetNumber', 'desiredCount', 'stage', 'operation', 'providerStatus', 'assigneeRead', 'observedAssignees', 'confirmedAssignments', 'remainingAssignments', 'mutation']), 'issue-assignment-count-invalid': Object.freeze([]), 'pull-request-link-restored': Object.freeze([]), 'pull-request-link-base-retained': Object.freeze([]), 'pull-request-link-reference-retained': Object.freeze([]), 'pull-request-link-base-and-reference-retained': Object.freeze([]), 'managed-branch-enrichment-failed': Object.freeze(['branchName']), 'inactivity-explanation-failed': Object.freeze(['issueNumber']), 'bugbot-review-blocks-pending': Object.freeze(['pendingCount']), 'branch-sync-agent-disabled': Object.freeze(['command']), 'branch-sync-fixer-unavailable': Object.freeze(['command']), 'branch-sync-sensitive-paths': Object.freeze(['command', 'conflictCount', 'sensitiveCount']), 'branch-sync-conflict-limit': Object.freeze(['command', 'conflictCount', 'limit']), 'branch-sync-resolution-invalid': Object.freeze(['validationCode', 'command']), 'branch-sync-verification-failed': Object.freeze(['stage', 'reason', 'command', 'commandIndex', 'childSha', 'restoration']), 'branch-sync-catalog-incompatible': Object.freeze(['phase', 'profile', 'field', 'capabilityNumber', 'abort']), 'branch-sync-workflow-write-required': Object.freeze(['reason', 'remoteState', 'originalSha', 'candidateSha', 'observedSha', 'pushAttempted']), 'branch-sync-publication-failed': Object.freeze(['reason', 'remoteState', 'originalSha', 'candidateSha', 'observedSha', 'pushAttempted']), 'sdd-draft-size-exceeded': Object.freeze([]), 'sdd-draft-response-too-short': Object.freeze(['minimum', 'observed', 'attempts']), 'sdd-catalog-unsupported-profile': Object.freeze([]), 'sdd-catalog-format-invalid': Object.freeze([]), 'sdd-catalog-missing-trusted-source': Object.freeze([]), 'sdd-source-context-unavailable': Object.freeze(['sourceCommit', 'phase', 'reason']), 'sdd-draft-format-invalid': Object.freeze([]), 'sdd-draft-case-identity-invalid': Object.freeze([]), 'sdd-draft-case-budget-invalid': Object.freeze([]), 'sdd-draft-requirement-mapping-invalid': Object.freeze([]), 'sdd-draft-repository-path-invalid': Object.freeze([]), 'progress-binary-evidence-unavailable': Object.freeze(['paths', 'pathCount']), 'progress-inventory-incomplete': Object.freeze(['profile', 'reason', 'observedCount', 'supportedLimit', 'countKind', 'paths']), 'progress-evidence-capacity-exceeded': Object.freeze(['phase', 'dimension', 'unit', 'bound', 'observedCount', 'supportedLimit', 'countKind', 'context']), 'bugbot-review-incomplete': Object.freeze(['profile', 'countKind', 'resource', 'observed', 'limit', 'unit']), 'progress-result-contract-invalid': Object.freeze(['phase', 'reason', 'field', 'requirementId', 'sourceLineId']), 'progress-partition-bytes-invalid': Object.freeze(['partitionId', 'filePath', 'unitId', 'field', 'maximum', 'observed']), 'progress-partition-length-invalid': Object.freeze(['partitionId', 'filePath', 'unitId', 'field', 'maximum', 'observed']), 'bugbot-partition-query-exhausted': Object.freeze(['reason', 'attempts']), 'bugbot-freshness-before-publication': Object.freeze(['operation', 'providerStatus', 'providerReference', 'reason']), 'bugbot-freshness-after-publication': Object.freeze(['operation', 'providerStatus', 'providerReference', 'reason']), 'progress-assessment-length-invalid': Object.freeze(['requirementId', 'sourceLineId', 'field', 'maximum', 'observed']), 'progress-assessment-capacity-exceeded': Object.freeze(['phase', 'reason', 'field', 'requirementId', 'sourceLineId']), 'progress-batch-retained': Object.freeze(['reason', 'verified', 'expected', 'batch', 'requirementId']), 'progress-sdd-seed-unverified': Object.freeze([]), 'progress-scope-clause-limit': Object.freeze(['clauseKind', 'observed', 'limit', 'sourceLineId']), 'progress-completion-recovery-exhausted': Object.freeze(['reportedProgress', 'requirementCount', 'supportedCount', 'missingCount', 'unknownCount']), 'deployment-publication-context-missing': Object.freeze([]), 'workspace-changes-rejected': Object.freeze([]), }); function normalizeApplicationErrorRecovery( recovery: ApplicationErrorRecovery | undefined, ): ApplicationErrorRecovery | undefined { if (!recovery) return undefined; if (!APPLICATION_ERROR_RECOVERY_IDS.includes(recovery.id)) { throw new TypeError('Application error recovery ID is invalid.'); } const variables = recovery.variables as Readonly>; const actualKeys = Object.keys(variables).sort(); const expectedKeys = [...RECOVERY_VARIABLE_KEYS[recovery.id]].sort(); if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) { throw new TypeError(`Application error recovery variables are invalid for ${recovery.id}.`); } if (recovery.id === 'progress-completion-recovery-exhausted') { return Object.freeze({ id: recovery.id, variables: normalizeProgressCompletionDiagnostic(variables) }); } if (recovery.id === 'progress-evidence-capacity-exceeded') { return Object.freeze({ id: recovery.id, variables: normalizeProgressCapacityDiagnostic(variables) }); } if (recovery.id === 'sdd-source-context-unavailable' && !isSddSourceContextDiagnostic(variables)) { throw new TypeError('SDD source context diagnostic is invalid.'); } if (recovery.id === 'sdd-draft-response-too-short' && !isSddDraftMinimumDiagnostic(variables)) { throw new TypeError('SDD minimum response diagnostic is invalid.'); } if (isCopilotCommandValidationReason(recovery.id) && !isValidCopilotCommandValidation(recovery.id, variables.command)) { throw new TypeError('Comment command validation diagnostic is invalid.'); } if (recovery.id.startsWith('stored-configuration-') && (![variables.issueNumber, variables.expectedVersion].every(value => typeof value === 'number' && Number.isSafeInteger(value) && value > 0) || (recovery.id === 'stored-configuration-schema-unsupported' && !(typeof variables.actualVersion === 'number' && Number.isSafeInteger(variables.actualVersion) && variables.actualVersion >= 0)))) { throw new TypeError('Stored configuration diagnostic is invalid.'); } if (recovery.id === 'managed-branch-enrichment-failed' && (typeof variables.branchName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/u.test(variables.branchName))) { throw new TypeError('Application error recovery branch name is invalid.'); } if (recovery.id === 'inactivity-explanation-failed' && (typeof variables.issueNumber !== 'number' || !Number.isSafeInteger(variables.issueNumber) || variables.issueNumber < 1)) { throw new TypeError('Application error recovery issue number is invalid.'); } if (recovery.id === 'bugbot-review-blocks-pending' && (typeof variables.pendingCount !== 'number' || !Number.isSafeInteger(variables.pendingCount) || variables.pendingCount < 1)) { throw new TypeError('Application error recovery pending count is invalid.'); } if ((recovery.id === 'branch-sync-agent-disabled' || recovery.id === 'branch-sync-fixer-unavailable' || recovery.id === 'branch-sync-sensitive-paths' || recovery.id === 'branch-sync-conflict-limit' || recovery.id === 'branch-sync-resolution-invalid') && variables.command !== '/copilot sync-branch') { throw new TypeError('Application error recovery synchronization command is invalid.'); } if (recovery.id === 'branch-sync-sensitive-paths' || recovery.id === 'branch-sync-conflict-limit') { const count = variables.conflictCount; if (typeof count !== 'number' || !Number.isSafeInteger(count) || count < 1) { throw new TypeError('Branch synchronization conflict count is invalid.'); } if (recovery.id === 'branch-sync-sensitive-paths' ? typeof variables.sensitiveCount !== 'number' || !Number.isSafeInteger(variables.sensitiveCount) || variables.sensitiveCount < 1 || variables.sensitiveCount > count : count <= 20 || variables.limit !== 20) { throw new TypeError('Branch synchronization refusal counts are invalid.'); } } if (recovery.id === 'branch-sync-resolution-invalid' && !BRANCH_SYNC_VALIDATION_CODES.some(code => code === variables.validationCode)) { throw new TypeError('Application error recovery synchronization validation code is invalid.'); } if (recovery.id === 'branch-sync-verification-failed' && !isBranchSyncVerificationFailure(variables)) { throw new TypeError('Branch synchronization verification diagnostic is invalid.'); } if (recovery.id === 'branch-sync-workflow-write-required' || recovery.id === 'branch-sync-publication-failed') { if (!isBranchSyncPublicationDiagnostic(variables) || variables.remoteState === 'published' || (recovery.id === 'branch-sync-workflow-write-required') !== (variables.reason === 'workflow-write-required')) { throw new TypeError('Branch synchronization publication diagnostic is invalid.'); } } if (recovery.id === 'branch-sync-catalog-incompatible') { const { abort, ...diagnostic } = variables; if (!isBranchSyncCatalogDiagnostic(diagnostic) || !['completed', 'unverified'].includes(String(abort))) { throw new TypeError('Branch synchronization catalog diagnostic is invalid.'); } } if (recovery.id === 'closed-issue-reconciliation-failed' && (!CLOSED_ISSUE_RESULTS.some(value => value === variables.resultId) || !CLOSED_ISSUE_PHASES.some(value => value === variables.phase) || !['open', 'closed', 'unavailable'].includes(variables.state as string) || !CLOSED_ISSUE_REASONS.some(value => value === variables.reason) || ![variables.changed, variables.remaining].every(value => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 3000))) { throw new TypeError('Closed issue reconciliation diagnostic is invalid.'); } if (recovery.id === 'approval-evidence-read-failed' && (!Object.prototype.hasOwnProperty.call(APPROVAL_EVIDENCE_PERMISSIONS, variables.operation) || !APPROVAL_EVIDENCE_REASONS.some(reason => reason === variables.reason) || APPROVAL_EVIDENCE_PERMISSIONS[variables.operation as keyof typeof APPROVAL_EVIDENCE_PERMISSIONS] !== variables.permission)) { throw new TypeError('Approval evidence diagnostic is invalid.'); } if (recovery.id === 'progress-assessment-capacity-exceeded' && (!isProgressResponseDiagnosticFacts(variables) || variables.phase !== 'assessment' || variables.reason !== 'assessment-text-limit')) { throw new TypeError('Progress assessment capacity diagnostic is invalid.'); } if (recovery.id === 'progress-batch-retained' && !isProgressBatchDiagnostic(variables)) throw new TypeError('Progress batch diagnostic is invalid.'); if (recovery.id === 'progress-scope-clause-limit' && !isProgressScopeCapacityFacts(variables)) throw new TypeError('Progress scope capacity diagnostic is invalid.'); if (recovery.id === 'progress-result-contract-invalid' && !isProgressResponseDiagnosticFacts(variables)) throw new TypeError('Progress response diagnostic is invalid.'); if (recovery.id === 'progress-partition-bytes-invalid' && !isProgressPartitionByteDiagnostic(variables)) throw new TypeError('Progress partition byte diagnostic is invalid.'); if (recovery.id === 'progress-partition-length-invalid' && !isProgressPartitionLengthDiagnostic(variables)) throw new TypeError('Progress part length diagnostic is invalid.'); if (recovery.id === 'progress-assessment-length-invalid' && !isProgressAssessmentLengthDiagnostic(variables)) throw new TypeError('Progress assessment length diagnostic is invalid.'); if (recovery.id === 'bugbot-partition-query-exhausted' && !isBugbotPartitionQueryDiagnostic(variables)) throw new TypeError('Bugbot partition query diagnostic is invalid.'); if (recovery.id === 'bugbot-review-incomplete' && !isBugbotReviewBlockFacts(variables)) throw new TypeError('Bugbot review diagnostic is invalid.'); if (recovery.id === 'progress-inventory-incomplete' && !isProgressInventoryDiagnostic(variables)) throw new TypeError('Progress inventory diagnostic is invalid.'); if (recovery.id === 'progress-binary-evidence-unavailable' && !isProgressBinaryEvidenceFacts(variables)) throw new TypeError('Progress binary evidence diagnostic is invalid.'); if (recovery.id === 'issue-workflow-admission-blocked' && !isIssueWorkflowDiagnostic(variables)) throw new TypeError('Issue admission diagnostic is invalid.'); if (recovery.id === 'change-size-reconciliation-failed' && !isChangeSizeReconciliationFacts(variables)) { throw new TypeError('Change size reconciliation diagnostic is invalid.'); } if (recovery.id === 'issue-assignment-failed' && !isIssueAssignmentDiagnostic(variables)) { throw new TypeError('Issue assignment diagnostic is invalid.'); } if ((recovery.id === 'bugbot-freshness-before-publication' || recovery.id === 'bugbot-freshness-after-publication') && !isBugbotFreshnessDiagnostic(variables)) { throw new TypeError('Bugbot freshness diagnostic is invalid.'); } return Object.freeze({ id: recovery.id, variables: Object.freeze({ ...variables }), }) as ApplicationErrorRecovery; }