From 0b19fe4390e968e9e945a28c1531e6e10be6dc4b Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Wed, 21 Oct 2026 14:00:20 +0200 Subject: [PATCH 1/4] fix: preserve actionable finding mutation diagnostics --- build/api/index.js | 134 +++++++++--- build/api/src/domain/bugbot/context.d.ts | 2 + build/cli/index.js | 199 +++++++++++++----- build/github_action/index.js | 199 +++++++++++++----- docs/bugbot/failure-scenarios.mdx | 7 + specs/CATALOG.md | 8 +- specs/bugbot-review-state-reconciliation.md | 69 ++++++ specs/catalog.json | 10 +- .../contracts/bugbot_reconciliation.ts | 2 + .../__tests__/bugbot_mutation_error.test.ts | 34 +++ .../errors/bugbot_mutation_error.ts | 35 +++ .../policies/bugbot_message_catalog.ts | 4 + .../bugbot_review_presentation_policy.ts | 3 + .../ports/pull_request_review_errors.ts | 13 +- ...detect_potential_problems_use_case.test.ts | 4 +- .../__tests__/bugbot_review_context.test.ts | 15 ++ .../finding_mutation_recovery.test.ts | 53 +++++ .../mark_findings_resolved_use_case.test.ts | 4 +- .../publish_findings_use_case.test.ts | 21 ++ ...le_bugbot_review_state_integration.test.ts | 32 +++ .../commit/bugbot/apply_detected_findings.ts | 3 +- .../commit/bugbot/bugbot_review_context.ts | 5 + .../bugbot/load_bugbot_context_use_case.ts | 1 + .../bugbot/mark_findings_resolved_workflow.ts | 31 ++- .../bugbot/publish_findings_use_case.ts | 35 +-- .../reconcile_bugbot_review_state_use_case.ts | 8 +- ...ize_bugbot_review_presentation_use_case.ts | 4 +- ..._review_comment_command_repository.test.ts | 8 + .../pull_request_changes_repository.ts | 2 +- ...quest_review_comment_command_repository.ts | 2 +- ...request_review_comment_query_repository.ts | 2 +- .../pull_request_review_error_metadata.ts | 16 ++ .../pull_request_review_thread_repository.ts | 2 +- .../pull_request_reviewer_repository.ts | 2 +- src/domain/bugbot/context.ts | 2 + 35 files changed, 780 insertions(+), 191 deletions(-) create mode 100644 src/application/errors/__tests__/bugbot_mutation_error.test.ts create mode 100644 src/application/errors/bugbot_mutation_error.ts create mode 100644 src/application/usecases/steps/commit/bugbot/__tests__/finding_mutation_recovery.test.ts create mode 100644 src/data/repository/pull_request/pull_request_review_error_metadata.ts diff --git a/build/api/index.js b/build/api/index.js index 02582bbcd..110efe745 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -68,6 +68,43 @@ function runAtApplicationErrorBoundary(operation) { } +/***/ }), + +/***/ 2937: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.BugbotMutationError = void 0; +exports.bugbotMutationError = bugbotMutationError; +const application_error_1 = __nccwpck_require__(5999); +const pull_request_review_errors_1 = __nccwpck_require__(6445); +class BugbotMutationError extends application_error_1.ApplicationError { + constructor(diagnostic) { + super(diagnostic.errorCode, `Bugbot ${diagnostic.stage} failed: operation=${diagnostic.operation}; finding=${diagnostic.findingId}; affected=${diagnostic.affectedCount}; ${diagnostic.providerStatus}; provider-reference=${diagnostic.providerReference}. Completed effects are retained; correct the named cause and recheck the same target.`); + this.diagnostic = diagnostic; + } +} +exports.BugbotMutationError = BugbotMutationError; +function bugbotMutationError(cause, stage, findingIds, fallback) { + if (cause instanceof BugbotMutationError) + return cause; + const provider = cause instanceof pull_request_review_errors_1.PullRequestReviewOperationError ? cause : undefined; + const status = provider?.providerStatus; + const errorCode = status === 401 ? 'authorization.credential-invalid' : status === 403 ? 'authorization.denied' + : status === 404 ? 'provider.not-found' : status === 409 ? 'provider.conflict' + : status === 429 ? 'provider.rate-limited' : status && status < 500 ? 'provider.contract-invalid' : 'provider.unavailable'; + const first = findingIds[0]; + return new BugbotMutationError(Object.freeze({ + stage, operation: provider?.operation ?? fallback, + findingId: typeof first === 'string' && /^[A-Za-z0-9_./:-]{1,200}$/.test(first) ? first : 'unavailable', + affectedCount: Math.min(findingIds.length, 10000), + providerStatus: status ? `http-${status}` : 'http-unavailable', + providerReference: provider?.providerReference ?? 'unavailable', errorCode, + })); +} + + /***/ }), /***/ 601: @@ -796,6 +833,7 @@ exports.BUGBOT_MESSAGE_IDS = Object.freeze([ 'bugbot.overflow.body', 'bugbot.common.more', 'bugbot.diagnostic.operationFailed', + 'bugbot.diagnostic.findingMutationFailed', 'bugbot.diagnostic.identityUnavailable', 'bugbot.diagnostic.pullRequestCommentsFailed', 'bugbot.diagnostic.reviewThreadsFailed', @@ -881,6 +919,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'bugbot.overflow.heading': 'More findings (comment limit)', 'bugbot.overflow.body': Object.freeze({ one: 'There is {count} more finding that was not published as an individual comment. Review locally or in the full diff to see the list.', other: 'There are {count} more findings that were not published as individual comments. Review locally or in the full diff to see the list.' }), 'bugbot.common.more': Object.freeze({ one: 'and {count} more.', other: 'and {count} more.' }), + 'bugbot.diagnostic.findingMutationFailed': 'Finding mutation failed: stage={stage}; operation={operation}; finding={findingId}; affected={affectedCount}; {providerStatus}; provider reference={providerReference}. Completed effects are retained. Correct access/contract errors before retrying; for transient failures restore provider availability, then recheck the same PR. Recheck reconciles existing reviews and threads.', 'bugbot.diagnostic.operationFailed': 'Bugbot could not complete one or more finding mutations.', 'bugbot.diagnostic.identityUnavailable': 'The authenticated Bugbot identity is unavailable.', 'bugbot.diagnostic.pullRequestCommentsFailed': 'Unable to re-read pull request review comments.', @@ -970,6 +1009,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'bugbot.overflow.heading': 'Más hallazgos (límite de comentarios)', 'bugbot.overflow.body': Object.freeze({ one: 'Hay {count} hallazgo más que no se publicó como comentario individual. Revísalo localmente o en el diff completo para consultar la lista.', many: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.', other: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.' }), 'bugbot.common.more': Object.freeze({ one: 'y {count} más.', many: 'y {count} más.', other: 'y {count} más.' }), + 'bugbot.diagnostic.findingMutationFailed': 'Falló una mutación: fase={stage}; operación={operation}; hallazgo={findingId}; afectados={affectedCount}; {providerStatus}; referencia del proveedor={providerReference}. Se conservan los efectos completados. Corrige acceso/contrato antes de reintentar; para fallos transitorios restaura el proveedor y revisa la misma PR. La revisión reconcilia comentarios e hilos existentes.', 'bugbot.diagnostic.operationFailed': 'Bugbot no pudo completar una o más mutaciones de hallazgos.', 'bugbot.diagnostic.identityUnavailable': 'La identidad autenticada de Bugbot no está disponible.', 'bugbot.diagnostic.pullRequestCommentsFailed': 'No se pudieron volver a leer los comentarios del review del pull request.', @@ -1009,6 +1049,7 @@ async function resolveBugbotCatalog(locale, configuration, resolver) { } function renderBugbotDiagnostic(diagnostic, catalog) { switch (diagnostic.code) { + case 'finding-mutation-failed': return catalog.message('bugbot.diagnostic.findingMutationFailed', { ...diagnostic.mutation }); case 'operation-failed': return catalog.message('bugbot.diagnostic.operationFailed'); case 'identity-unavailable': return catalog.message('bugbot.diagnostic.identityUnavailable'); case 'snapshot-pull-request-comments-failed': return catalog.message('bugbot.diagnostic.pullRequestCommentsFailed'); @@ -1558,6 +1599,10 @@ function renderBugbotStatusCard(projection, catalogOrLocale, links) { facts.push(catalog.message('bugbot.coverage.omitted', { count: source.omittedItems })); if (source.truncatedItems > 0) facts.push(catalog.message('bugbot.coverage.truncated', { count: source.truncatedItems })); + const truncatedIds = source.truncatedItemIds?.slice(0, 50) + .filter(id => /^(?:issue:[0-9]+|review:[A-Za-z0-9_=-]+)$/.test(id) && id.length <= 160); + if (truncatedIds?.length) + facts.push(`truncated-source-ids=${truncatedIds.join(', ')}`); if (source.providerLimitReached) facts.push(catalog.message('bugbot.coverage.providerLimit')); return `- ${source.source}: ${source.status}; ${facts.join(', ')}`; @@ -2136,6 +2181,13 @@ class PullRequestReviewOperationError extends Error { super(buildMessage(operation, context)); this.name = "PullRequestReviewOperationError"; this.operation = operation; + const status = context?.providerStatus; + if (typeof status === 'number' && Number.isInteger(status) && status >= 400 && status <= 599) { + this.providerStatus = status; + } + if (typeof context?.providerReference === 'string' && /^[A-Za-z0-9:-]{1,64}$/.test(context.providerReference)) { + this.providerReference = context.providerReference; + } } } exports.PullRequestReviewOperationError = PullRequestReviewOperationError; @@ -2254,6 +2306,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.applyDetectedFindings = applyDetectedFindings; const mark_findings_resolved_use_case_1 = __nccwpck_require__(6963); const publish_findings_use_case_1 = __nccwpck_require__(8442); +const bugbot_mutation_error_1 = __nccwpck_require__(2937); const pull_request_review_errors_1 = __nccwpck_require__(6445); async function applyDetectedFindings(operation, context, prepared, publicationPorts, resolutionPorts, catalog) { try { @@ -2269,7 +2322,7 @@ async function applyDetectedFindings(operation, context, prepared, publicationPo }); } catch (error) { - const publicationError = error instanceof pull_request_review_errors_1.PullRequestReviewOperationError + const publicationError = error instanceof bugbot_mutation_error_1.BugbotMutationError || error instanceof pull_request_review_errors_1.PullRequestReviewOperationError ? error : new Error("Unable to publish findings."); return [publicationError]; @@ -2658,6 +2711,10 @@ function buildReviewConversationContext(issueComments, commentsByPullRequest, bo omitted, truncated: chronological.filter((entry) => entry.truncated).length, retained: chronological.length, + ...(chronological.some(entry => entry.truncated) ? { + truncatedItemIds: chronological.filter(entry => entry.truncated) + .map(entry => entry.providerId).filter(id => /^(?:issue:[0-9]+|review:[A-Za-z0-9_=-]+)$/.test(id) && id.length <= 160), + } : {}), }; } function appendConversationEntry(entries, author, kind, body, createdAt, providerId) { @@ -3339,6 +3396,7 @@ async function loadBugbotContext(request, ports, resolvedPreflight) { itemsFetched: conversationContext.retained + conversationContext.omitted, omittedItems: conversationContext.omitted, truncatedItems: conversationContext.truncated, + ...(conversationContext.truncatedItemIds ? { truncatedItemIds: conversationContext.truncatedItemIds } : {}), limitReached: conversationContext.omitted > 0 || conversationContext.truncated > 0, }, { @@ -3537,11 +3595,11 @@ Object.defineProperty(exports, "markFindingsResolved", ({ enumerable: true, get: Object.defineProperty(exports, "__esModule", ({ value: true })); exports.markFindingsResolved = markFindingsResolved; -const pull_request_review_errors_1 = __nccwpck_require__(6445); const logging_ports_1 = __nccwpck_require__(6152); const resolve_issue_finding_1 = __nccwpck_require__(5300); const resolve_pull_request_finding_1 = __nccwpck_require__(4567); const review_state_1 = __nccwpck_require__(9200); +const bugbot_mutation_error_1 = __nccwpck_require__(2937); const application_error_1 = __nccwpck_require__(5999); async function markFindingsResolved(param) { const errors = []; @@ -3569,8 +3627,8 @@ async function repairExistingPullRequestFinding(ports, operation, findingId, des try { await ports.pullRequestComments.unresolvePullRequestReviewThread(destination.pullRequestNumber, destination.commentIdentity); } - catch { - addResolutionError(errors, 'pull request'); + catch (cause) { + addResolutionError(errors, 'pull request', findingId, cause); } } } @@ -3584,7 +3642,7 @@ async function resolveIssueIfNeeded(param, findingId, destination, errors) { return; const comment = param.context.issueComments.find(item => item.id === destination.commentId); if (comment?.body == null) { - addResolutionError(errors, 'issue'); + addResolutionError(errors, 'issue', findingId); return; } try { @@ -3595,8 +3653,8 @@ async function resolveIssueIfNeeded(param, findingId, destination, errors) { resolution: param.resolvedFindingResolutions?.get(findingId), }, param.catalog); } - catch { - addResolutionError(errors, 'issue'); + catch (cause) { + addResolutionError(errors, 'issue', findingId, cause); } } async function tryResolvePullRequestFinding(ports, findingId, destination, errors, resolution, catalog) { @@ -3608,17 +3666,14 @@ async function tryResolvePullRequestFinding(ports, findingId, destination, error resolution, }, catalog); } - catch { - addResolutionError(errors, 'pull request'); + catch (cause) { + addResolutionError(errors, 'pull request', findingId, cause); } } -function addResolutionError(errors, destination) { - const cause = destination === 'pull request' - ? new pull_request_review_errors_1.PullRequestReviewOperationError('mark-resolved') - : new Error('Unable to mark an issue finding as resolved.'); - const semanticError = new application_error_1.ApplicationError('provider.unavailable', destination === 'pull request' - ? 'Unable to mark a pull request finding as resolved.' - : 'Unable to mark an issue finding as resolved.', { cause }); +function addResolutionError(errors, destination, findingId, cause) { + const semanticError = destination === 'pull request' + ? (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'resolution', [findingId], 'mark-resolved') + : new application_error_1.ApplicationError('provider.unavailable', 'Unable to mark an issue finding as resolved.'); (0, logging_ports_1.logError)(semanticError); errors.push(semanticError); } @@ -3867,6 +3922,7 @@ function isRecord(value) { */ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.publishFindings = publishFindings; +const bugbot_mutation_error_1 = __nccwpck_require__(2937); const finding_1 = __nccwpck_require__(1011); const publish_issue_finding_comment_1 = __nccwpck_require__(4950); const publish_pr_review_comments_1 = __nccwpck_require__(352); @@ -3886,14 +3942,24 @@ async function publishFindings(param) { }) : undefined; for (const finding of findings) { - if (operation.target.issueNumber > 0 && !reviewPublisher) { - await (0, publish_issue_finding_comment_1.publishIssueFindingComment)(ports.issueComments, operation.target.issueNumber, finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding), commitSha, catalog); + try { + if (operation.target.issueNumber > 0 && !reviewPublisher) { + await (0, publish_issue_finding_comment_1.publishIssueFindingComment)(ports.issueComments, operation.target.issueNumber, finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding), commitSha, catalog); + } + if (reviewPublisher) { + await reviewPublisher.publish(finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding)); + } } - if (reviewPublisher) { - await reviewPublisher.publish(finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding)); + catch (cause) { + throw (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'publication', [finding.id], 'publish-comments'); } } - await reviewPublisher?.flush(overflowCount, overflowTitles); + try { + await reviewPublisher?.flush(overflowCount, overflowTitles); + } + catch (cause) { + throw (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'publication', findings.map(finding => finding.id), 'publish-comments'); + } if (operation.target.issueNumber > 0 && !reviewPublisher) { await (0, publish_overflow_comment_1.publishOverflowComment)(ports.issueComments, operation.target.issueNumber, overflowCount, overflowTitles, commitSha, catalog); } @@ -4188,6 +4254,7 @@ async function queryBugbotPartitionFindings(repository, configuration, prompt, t Object.defineProperty(exports, "__esModule", ({ value: true })); exports.reconcileBugbotReviewState = reconcileBugbotReviewState; +const bugbot_mutation_error_1 = __nccwpck_require__(2937); const review_projection_1 = __nccwpck_require__(859); const bugbot_reconciliation_policy_1 = __nccwpck_require__(8128); const bugbot_provider_projection_policy_1 = __nccwpck_require__(5821); @@ -4219,10 +4286,9 @@ async function reconcileBugbotReviewState(input) { } const snapshot = snapshotResult.snapshot; const diagnostics = [ - ...(input.mutationErrors ?? []).map((error) => ({ - code: 'operation-failed', - operatorMessage: toSafeOperationMessage(error), - })), + ...(input.mutationErrors ?? []).map((error) => error instanceof bugbot_mutation_error_1.BugbotMutationError + ? { code: 'finding-mutation-failed', mutation: error.diagnostic } + : { code: 'operation-failed', operatorMessage: toSafeOperationMessage(error) }), ...(!input.target.trustedAuthorLogin?.trim() ? [{ code: 'identity-unavailable' }] : []), @@ -4697,14 +4763,16 @@ function toPresentationFailure(diagnostic) { const message = (0, bugbot_message_catalog_1.bugbotDiagnosticOperatorMessage)(diagnostic); return { diagnostic, - error: diagnostic.code === 'review-updates-pending' - ? new application_error_1.ApplicationError('workflow.presentation-pending', message, { - recovery: { - id: 'bugbot-review-blocks-pending', - variables: { pendingCount: diagnostic.count }, - }, - }) - : new Error(message), + error: diagnostic.code === 'finding-mutation-failed' + ? new application_error_1.ApplicationError(diagnostic.mutation.errorCode, message) + : diagnostic.code === 'review-updates-pending' + ? new application_error_1.ApplicationError('workflow.presentation-pending', message, { + recovery: { + id: 'bugbot-review-blocks-pending', + variables: { pendingCount: diagnostic.count }, + }, + }) + : new Error(message), }; } function report(projection, reviewUpdates, pendingReviewUpdates, statusCardOperation, errors) { diff --git a/build/api/src/domain/bugbot/context.d.ts b/build/api/src/domain/bugbot/context.d.ts index f4ee3769b..144b133c7 100644 --- a/build/api/src/domain/bugbot/context.d.ts +++ b/build/api/src/domain/bugbot/context.d.ts @@ -8,6 +8,8 @@ export interface BugbotSourceCoverage { readonly itemsRetained: number; readonly omittedItems: number; readonly truncatedItems: number; + /** Exact bounded provider identities of retained conversation items truncated by the prompt. */ + readonly truncatedItemIds?: readonly string[]; readonly limitReached: boolean; /** True when a provider page cap hides an unknown number of older records. */ readonly providerLimitReached?: boolean; diff --git a/build/cli/index.js b/build/cli/index.js index addfe08bd..ed8b3b1ad 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -38119,6 +38119,44 @@ function approvalDiagnosticError(diagnostic) { } +/***/ }), + +/***/ 82937: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.BugbotMutationError = void 0; +exports.bugbotMutationError = bugbotMutationError; +const application_error_1 = __nccwpck_require__(75999); +const pull_request_review_errors_1 = __nccwpck_require__(46445); +class BugbotMutationError extends application_error_1.ApplicationError { + constructor(diagnostic) { + super(diagnostic.errorCode, `Bugbot ${diagnostic.stage} failed: operation=${diagnostic.operation}; finding=${diagnostic.findingId}; affected=${diagnostic.affectedCount}; ${diagnostic.providerStatus}; provider-reference=${diagnostic.providerReference}. Completed effects are retained; correct the named cause and recheck the same target.`); + this.diagnostic = diagnostic; + } +} +exports.BugbotMutationError = BugbotMutationError; +function bugbotMutationError(cause, stage, findingIds, fallback) { + if (cause instanceof BugbotMutationError) + return cause; + const provider = cause instanceof pull_request_review_errors_1.PullRequestReviewOperationError ? cause : undefined; + const status = provider?.providerStatus; + const errorCode = status === 401 ? 'authorization.credential-invalid' : status === 403 ? 'authorization.denied' + : status === 404 ? 'provider.not-found' : status === 409 ? 'provider.conflict' + : status === 429 ? 'provider.rate-limited' : status && status < 500 ? 'provider.contract-invalid' : 'provider.unavailable'; + const first = findingIds[0]; + return new BugbotMutationError(Object.freeze({ + stage, operation: provider?.operation ?? fallback, + findingId: typeof first === 'string' && /^[A-Za-z0-9_./:-]{1,200}$/.test(first) ? first : 'unavailable', + affectedCount: Math.min(findingIds.length, 10000), + providerStatus: status ? `http-${status}` : 'http-unavailable', + providerReference: provider?.providerReference ?? 'unavailable', errorCode, + })); +} + + /***/ }), /***/ 12183: @@ -40679,6 +40717,7 @@ exports.BUGBOT_MESSAGE_IDS = Object.freeze([ 'bugbot.overflow.body', 'bugbot.common.more', 'bugbot.diagnostic.operationFailed', + 'bugbot.diagnostic.findingMutationFailed', 'bugbot.diagnostic.identityUnavailable', 'bugbot.diagnostic.pullRequestCommentsFailed', 'bugbot.diagnostic.reviewThreadsFailed', @@ -40764,6 +40803,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'bugbot.overflow.heading': 'More findings (comment limit)', 'bugbot.overflow.body': Object.freeze({ one: 'There is {count} more finding that was not published as an individual comment. Review locally or in the full diff to see the list.', other: 'There are {count} more findings that were not published as individual comments. Review locally or in the full diff to see the list.' }), 'bugbot.common.more': Object.freeze({ one: 'and {count} more.', other: 'and {count} more.' }), + 'bugbot.diagnostic.findingMutationFailed': 'Finding mutation failed: stage={stage}; operation={operation}; finding={findingId}; affected={affectedCount}; {providerStatus}; provider reference={providerReference}. Completed effects are retained. Correct access/contract errors before retrying; for transient failures restore provider availability, then recheck the same PR. Recheck reconciles existing reviews and threads.', 'bugbot.diagnostic.operationFailed': 'Bugbot could not complete one or more finding mutations.', 'bugbot.diagnostic.identityUnavailable': 'The authenticated Bugbot identity is unavailable.', 'bugbot.diagnostic.pullRequestCommentsFailed': 'Unable to re-read pull request review comments.', @@ -40853,6 +40893,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'bugbot.overflow.heading': 'Más hallazgos (límite de comentarios)', 'bugbot.overflow.body': Object.freeze({ one: 'Hay {count} hallazgo más que no se publicó como comentario individual. Revísalo localmente o en el diff completo para consultar la lista.', many: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.', other: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.' }), 'bugbot.common.more': Object.freeze({ one: 'y {count} más.', many: 'y {count} más.', other: 'y {count} más.' }), + 'bugbot.diagnostic.findingMutationFailed': 'Falló una mutación: fase={stage}; operación={operation}; hallazgo={findingId}; afectados={affectedCount}; {providerStatus}; referencia del proveedor={providerReference}. Se conservan los efectos completados. Corrige acceso/contrato antes de reintentar; para fallos transitorios restaura el proveedor y revisa la misma PR. La revisión reconcilia comentarios e hilos existentes.', 'bugbot.diagnostic.operationFailed': 'Bugbot no pudo completar una o más mutaciones de hallazgos.', 'bugbot.diagnostic.identityUnavailable': 'La identidad autenticada de Bugbot no está disponible.', 'bugbot.diagnostic.pullRequestCommentsFailed': 'No se pudieron volver a leer los comentarios del review del pull request.', @@ -40892,6 +40933,7 @@ async function resolveBugbotCatalog(locale, configuration, resolver) { } function renderBugbotDiagnostic(diagnostic, catalog) { switch (diagnostic.code) { + case 'finding-mutation-failed': return catalog.message('bugbot.diagnostic.findingMutationFailed', { ...diagnostic.mutation }); case 'operation-failed': return catalog.message('bugbot.diagnostic.operationFailed'); case 'identity-unavailable': return catalog.message('bugbot.diagnostic.identityUnavailable'); case 'snapshot-pull-request-comments-failed': return catalog.message('bugbot.diagnostic.pullRequestCommentsFailed'); @@ -41519,6 +41561,10 @@ function renderBugbotStatusCard(projection, catalogOrLocale, links) { facts.push(catalog.message('bugbot.coverage.omitted', { count: source.omittedItems })); if (source.truncatedItems > 0) facts.push(catalog.message('bugbot.coverage.truncated', { count: source.truncatedItems })); + const truncatedIds = source.truncatedItemIds?.slice(0, 50) + .filter(id => /^(?:issue:[0-9]+|review:[A-Za-z0-9_=-]+)$/.test(id) && id.length <= 160); + if (truncatedIds?.length) + facts.push(`truncated-source-ids=${truncatedIds.join(', ')}`); if (source.providerLimitReached) facts.push(catalog.message('bugbot.coverage.providerLimit')); return `- ${source.source}: ${source.status}; ${facts.join(', ')}`; @@ -53459,6 +53505,13 @@ class PullRequestReviewOperationError extends Error { super(buildMessage(operation, context)); this.name = "PullRequestReviewOperationError"; this.operation = operation; + const status = context?.providerStatus; + if (typeof status === 'number' && Number.isInteger(status) && status >= 400 && status <= 599) { + this.providerStatus = status; + } + if (typeof context?.providerReference === 'string' && /^[A-Za-z0-9:-]{1,64}$/.test(context.providerReference)) { + this.providerReference = context.providerReference; + } } } exports.PullRequestReviewOperationError = PullRequestReviewOperationError; @@ -64546,6 +64599,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.applyDetectedFindings = applyDetectedFindings; const mark_findings_resolved_use_case_1 = __nccwpck_require__(96963); const publish_findings_use_case_1 = __nccwpck_require__(88442); +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const pull_request_review_errors_1 = __nccwpck_require__(46445); async function applyDetectedFindings(operation, context, prepared, publicationPorts, resolutionPorts, catalog) { try { @@ -64561,7 +64615,7 @@ async function applyDetectedFindings(operation, context, prepared, publicationPo }); } catch (error) { - const publicationError = error instanceof pull_request_review_errors_1.PullRequestReviewOperationError + const publicationError = error instanceof bugbot_mutation_error_1.BugbotMutationError || error instanceof pull_request_review_errors_1.PullRequestReviewOperationError ? error : new Error("Unable to publish findings."); return [publicationError]; @@ -65231,6 +65285,10 @@ function buildReviewConversationContext(issueComments, commentsByPullRequest, bo omitted, truncated: chronological.filter((entry) => entry.truncated).length, retained: chronological.length, + ...(chronological.some(entry => entry.truncated) ? { + truncatedItemIds: chronological.filter(entry => entry.truncated) + .map(entry => entry.providerId).filter(id => /^(?:issue:[0-9]+|review:[A-Za-z0-9_=-]+)$/.test(id) && id.length <= 160), + } : {}), }; } function appendConversationEntry(entries, author, kind, body, createdAt, providerId) { @@ -66722,6 +66780,7 @@ async function loadBugbotContext(request, ports, resolvedPreflight) { itemsFetched: conversationContext.retained + conversationContext.omitted, omittedItems: conversationContext.omitted, truncatedItems: conversationContext.truncated, + ...(conversationContext.truncatedItemIds ? { truncatedItemIds: conversationContext.truncatedItemIds } : {}), limitReached: conversationContext.omitted > 0 || conversationContext.truncated > 0, }, { @@ -66923,11 +66982,11 @@ Object.defineProperty(exports, "markFindingsResolved", ({ enumerable: true, get: Object.defineProperty(exports, "__esModule", ({ value: true })); exports.markFindingsResolved = markFindingsResolved; -const pull_request_review_errors_1 = __nccwpck_require__(46445); const logging_ports_1 = __nccwpck_require__(6152); const resolve_issue_finding_1 = __nccwpck_require__(35300); const resolve_pull_request_finding_1 = __nccwpck_require__(64567); const review_state_1 = __nccwpck_require__(79200); +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const application_error_1 = __nccwpck_require__(75999); async function markFindingsResolved(param) { const errors = []; @@ -66955,8 +67014,8 @@ async function repairExistingPullRequestFinding(ports, operation, findingId, des try { await ports.pullRequestComments.unresolvePullRequestReviewThread(destination.pullRequestNumber, destination.commentIdentity); } - catch { - addResolutionError(errors, 'pull request'); + catch (cause) { + addResolutionError(errors, 'pull request', findingId, cause); } } } @@ -66970,7 +67029,7 @@ async function resolveIssueIfNeeded(param, findingId, destination, errors) { return; const comment = param.context.issueComments.find(item => item.id === destination.commentId); if (comment?.body == null) { - addResolutionError(errors, 'issue'); + addResolutionError(errors, 'issue', findingId); return; } try { @@ -66981,8 +67040,8 @@ async function resolveIssueIfNeeded(param, findingId, destination, errors) { resolution: param.resolvedFindingResolutions?.get(findingId), }, param.catalog); } - catch { - addResolutionError(errors, 'issue'); + catch (cause) { + addResolutionError(errors, 'issue', findingId, cause); } } async function tryResolvePullRequestFinding(ports, findingId, destination, errors, resolution, catalog) { @@ -66994,17 +67053,14 @@ async function tryResolvePullRequestFinding(ports, findingId, destination, error resolution, }, catalog); } - catch { - addResolutionError(errors, 'pull request'); + catch (cause) { + addResolutionError(errors, 'pull request', findingId, cause); } } -function addResolutionError(errors, destination) { - const cause = destination === 'pull request' - ? new pull_request_review_errors_1.PullRequestReviewOperationError('mark-resolved') - : new Error('Unable to mark an issue finding as resolved.'); - const semanticError = new application_error_1.ApplicationError('provider.unavailable', destination === 'pull request' - ? 'Unable to mark a pull request finding as resolved.' - : 'Unable to mark an issue finding as resolved.', { cause }); +function addResolutionError(errors, destination, findingId, cause) { + const semanticError = destination === 'pull request' + ? (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'resolution', [findingId], 'mark-resolved') + : new application_error_1.ApplicationError('provider.unavailable', 'Unable to mark an issue finding as resolved.'); (0, logging_ports_1.logError)(semanticError); errors.push(semanticError); } @@ -67257,6 +67313,7 @@ function isRecord(value) { */ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.publishFindings = publishFindings; +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const finding_1 = __nccwpck_require__(31011); const publish_issue_finding_comment_1 = __nccwpck_require__(84950); const publish_pr_review_comments_1 = __nccwpck_require__(50352); @@ -67276,14 +67333,24 @@ async function publishFindings(param) { }) : undefined; for (const finding of findings) { - if (operation.target.issueNumber > 0 && !reviewPublisher) { - await (0, publish_issue_finding_comment_1.publishIssueFindingComment)(ports.issueComments, operation.target.issueNumber, finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding), commitSha, catalog); + try { + if (operation.target.issueNumber > 0 && !reviewPublisher) { + await (0, publish_issue_finding_comment_1.publishIssueFindingComment)(ports.issueComments, operation.target.issueNumber, finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding), commitSha, catalog); + } + if (reviewPublisher) { + await reviewPublisher.publish(finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding)); + } } - if (reviewPublisher) { - await reviewPublisher.publish(finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding)); + catch (cause) { + throw (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'publication', [finding.id], 'publish-comments'); } } - await reviewPublisher?.flush(overflowCount, overflowTitles); + try { + await reviewPublisher?.flush(overflowCount, overflowTitles); + } + catch (cause) { + throw (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'publication', findings.map(finding => finding.id), 'publish-comments'); + } if (operation.target.issueNumber > 0 && !reviewPublisher) { await (0, publish_overflow_comment_1.publishOverflowComment)(ports.issueComments, operation.target.issueNumber, overflowCount, overflowTitles, commitSha, catalog); } @@ -67583,6 +67650,7 @@ async function queryBugbotPartitionFindings(repository, configuration, prompt, t Object.defineProperty(exports, "__esModule", ({ value: true })); exports.reconcileBugbotReviewState = reconcileBugbotReviewState; +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const review_projection_1 = __nccwpck_require__(80859); const bugbot_reconciliation_policy_1 = __nccwpck_require__(78128); const bugbot_provider_projection_policy_1 = __nccwpck_require__(85821); @@ -67614,10 +67682,9 @@ async function reconcileBugbotReviewState(input) { } const snapshot = snapshotResult.snapshot; const diagnostics = [ - ...(input.mutationErrors ?? []).map((error) => ({ - code: 'operation-failed', - operatorMessage: toSafeOperationMessage(error), - })), + ...(input.mutationErrors ?? []).map((error) => error instanceof bugbot_mutation_error_1.BugbotMutationError + ? { code: 'finding-mutation-failed', mutation: error.diagnostic } + : { code: 'operation-failed', operatorMessage: toSafeOperationMessage(error) }), ...(!input.target.trustedAuthorLogin?.trim() ? [{ code: 'identity-unavailable' }] : []), @@ -68141,14 +68208,16 @@ function toPresentationFailure(diagnostic) { const message = (0, bugbot_message_catalog_1.bugbotDiagnosticOperatorMessage)(diagnostic); return { diagnostic, - error: diagnostic.code === 'review-updates-pending' - ? new application_error_1.ApplicationError('workflow.presentation-pending', message, { - recovery: { - id: 'bugbot-review-blocks-pending', - variables: { pendingCount: diagnostic.count }, - }, - }) - : new Error(message), + error: diagnostic.code === 'finding-mutation-failed' + ? new application_error_1.ApplicationError(diagnostic.mutation.errorCode, message) + : diagnostic.code === 'review-updates-pending' + ? new application_error_1.ApplicationError('workflow.presentation-pending', message, { + recovery: { + id: 'bugbot-review-blocks-pending', + variables: { pendingCount: diagnostic.count }, + }, + }) + : new Error(message), }; } function report(projection, reviewUpdates, pendingReviewUpdates, statusCardOperation, errors) { @@ -87051,8 +87120,8 @@ function collectDiagnostic(diagnostics, cause) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestChangesRepository = void 0; +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const logger_1 = __nccwpck_require__(91151); -const pull_request_review_errors_1 = __nccwpck_require__(46445); const github_pagination_policy_1 = __nccwpck_require__(44812); const application_error_1 = __nccwpck_require__(75999); class PullRequestChangesRepository { @@ -87065,7 +87134,7 @@ class PullRequestChangesRepository { } catch (error) { (0, logger_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to read the pull request review diff.')); - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-files'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-files'); } }; this.getBoundedBugbotReviewDiffSnapshot = async (owner, repository, pullNumber, token) => { @@ -87107,7 +87176,7 @@ class PullRequestChangesRepository { } catch (error) { (0, logger_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to read the pull request review diff.')); - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-files'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-files'); } }; /** Head commit SHA of the PR (for creating review). */ @@ -87126,7 +87195,7 @@ class PullRequestChangesRepository { } catch (error) { (0, logger_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to read the pull request head SHA.')); - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "get-head-sha"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "get-head-sha"); } }; } @@ -87469,6 +87538,7 @@ function toBugbotPullRequestIdentity(value) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewCommentCommandRepository = void 0; +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const pull_request_review_errors_1 = __nccwpck_require__(46445); const github_pagination_policy_1 = __nccwpck_require__(44812); class PullRequestReviewCommentCommandRepository { @@ -87537,7 +87607,7 @@ class PullRequestReviewCommentCommandRepository { const context = comments.length > 0 ? { failedCount: comments.length, totalCount: comments.length } : undefined; - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "publish-comments", context); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "publish-comments", context); } } async updatePullRequestReview(owner, repository, pullRequestNumber, reviewIdentity, body, token) { @@ -87558,7 +87628,7 @@ class PullRequestReviewCommentCommandRepository { throw new pull_request_review_errors_1.PullRequestReviewOperationError('update-review'); } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'update-review'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'update-review'); } } async updatePullRequestReviewComment(_owner, _repository, commentIdentity, body, token) { @@ -87577,7 +87647,7 @@ class PullRequestReviewCommentCommandRepository { } } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "update-comment"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "update-comment"); } } } @@ -87593,7 +87663,7 @@ exports.PullRequestReviewCommentCommandRepository = PullRequestReviewCommentComm Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewCommentQueryRepository = void 0; -const pull_request_review_errors_1 = __nccwpck_require__(46445); +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const github_pagination_policy_1 = __nccwpck_require__(44812); function toReviewComment(comment) { if (typeof comment.node_id !== "string" || comment.node_id.length === 0) { @@ -87649,7 +87719,7 @@ class PullRequestReviewCommentQueryRepository { return comments; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "list-comments"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "list-comments"); } } async listBugbotPullRequestReviewCommentsBounded(owner, repository, pullRequestNumber, token) { @@ -87692,7 +87762,7 @@ class PullRequestReviewCommentQueryRepository { }; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "list-comments"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "list-comments"); } } async listPullRequestReviews(owner, repository, pullRequestNumber, token) { @@ -87706,7 +87776,7 @@ class PullRequestReviewCommentQueryRepository { return reviews; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-reviews'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-reviews'); } } async getPullRequestReviewCommentBody(owner, repository, _pullRequestNumber, commentId, token) { @@ -87720,13 +87790,37 @@ class PullRequestReviewCommentQueryRepository { return data.body ?? null; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "get-comment"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "get-comment"); } } } exports.PullRequestReviewCommentQueryRepository = PullRequestReviewCommentQueryRepository; +/***/ }), + +/***/ 54354: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPullRequestReviewOperationError = toPullRequestReviewOperationError; +const pull_request_review_errors_1 = __nccwpck_require__(46445); +/** Only safe HTTP metadata crosses the provider boundary; provider prose never does. */ +function toPullRequestReviewOperationError(error, operation, context) { + if (!error || typeof error !== 'object') + return (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, operation, context); + const record = error; + return (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, operation, { + ...context, + ...(typeof record.status === 'number' ? { providerStatus: record.status } : {}), + ...(typeof record.response?.headers?.['x-github-request-id'] === 'string' + ? { providerReference: record.response.headers['x-github-request-id'] } : {}), + }); +} + + /***/ }), /***/ 2307: @@ -87837,6 +87931,7 @@ function nextConnectionCursor(pageInfo, seenCursors) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewThreadRepository = void 0; +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const logger_1 = __nccwpck_require__(91151); const pull_request_review_errors_1 = __nccwpck_require__(46445); const pull_request_review_thread_locator_1 = __nccwpck_require__(2307); @@ -87884,7 +87979,7 @@ class PullRequestReviewThreadRepository { return states; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-threads'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-threads'); } }; this.listBugbotPullRequestReviewThreadStatesBounded = async (owner, repository, pullNumber, token) => { @@ -87953,7 +88048,7 @@ class PullRequestReviewThreadRepository { }; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-threads'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-threads'); } }; this.resolvePullRequestReviewThread = async (owner, repository, pullNumber, commentIdentity, token) => { @@ -87977,7 +88072,7 @@ class PullRequestReviewThreadRepository { (0, logger_1.logDebugInfo)('Resolved pull request review thread.'); } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'resolve-thread'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'resolve-thread'); } }; this.unresolvePullRequestReviewThread = async (owner, repository, pullNumber, commentIdentity, token) => { @@ -88001,7 +88096,7 @@ class PullRequestReviewThreadRepository { (0, logger_1.logDebugInfo)('Reopened pull request review thread.'); } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'unresolve-thread'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'unresolve-thread'); } }; } @@ -88018,7 +88113,7 @@ exports.PullRequestReviewThreadRepository = PullRequestReviewThreadRepository; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewerRepository = void 0; -const pull_request_review_errors_1 = __nccwpck_require__(46445); +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const github_pagination_policy_1 = __nccwpck_require__(44812); const COMPLETED_REVIEW_STATES = new Set([ "APPROVED", @@ -88051,7 +88146,7 @@ class PullRequestReviewerRepository { return [...reviewers.values()]; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "list-reviewers"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "list-reviewers"); } } async addReviewersToPullRequest(owner, repository, pullRequestNumber, reviewers, token) { @@ -88076,7 +88171,7 @@ class PullRequestReviewerRepository { return [...confirmed.values()]; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "request-reviewers"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "request-reviewers"); } } async listRequestedReviewers(client, parameters) { diff --git a/build/github_action/index.js b/build/github_action/index.js index 9cd6d5ace..c1e6a9d3b 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -38866,6 +38866,44 @@ function approvalDiagnosticError(diagnostic) { } +/***/ }), + +/***/ 82937: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.BugbotMutationError = void 0; +exports.bugbotMutationError = bugbotMutationError; +const application_error_1 = __nccwpck_require__(75999); +const pull_request_review_errors_1 = __nccwpck_require__(46445); +class BugbotMutationError extends application_error_1.ApplicationError { + constructor(diagnostic) { + super(diagnostic.errorCode, `Bugbot ${diagnostic.stage} failed: operation=${diagnostic.operation}; finding=${diagnostic.findingId}; affected=${diagnostic.affectedCount}; ${diagnostic.providerStatus}; provider-reference=${diagnostic.providerReference}. Completed effects are retained; correct the named cause and recheck the same target.`); + this.diagnostic = diagnostic; + } +} +exports.BugbotMutationError = BugbotMutationError; +function bugbotMutationError(cause, stage, findingIds, fallback) { + if (cause instanceof BugbotMutationError) + return cause; + const provider = cause instanceof pull_request_review_errors_1.PullRequestReviewOperationError ? cause : undefined; + const status = provider?.providerStatus; + const errorCode = status === 401 ? 'authorization.credential-invalid' : status === 403 ? 'authorization.denied' + : status === 404 ? 'provider.not-found' : status === 409 ? 'provider.conflict' + : status === 429 ? 'provider.rate-limited' : status && status < 500 ? 'provider.contract-invalid' : 'provider.unavailable'; + const first = findingIds[0]; + return new BugbotMutationError(Object.freeze({ + stage, operation: provider?.operation ?? fallback, + findingId: typeof first === 'string' && /^[A-Za-z0-9_./:-]{1,200}$/.test(first) ? first : 'unavailable', + affectedCount: Math.min(findingIds.length, 10000), + providerStatus: status ? `http-${status}` : 'http-unavailable', + providerReference: provider?.providerReference ?? 'unavailable', errorCode, + })); +} + + /***/ }), /***/ 12183: @@ -41967,6 +42005,7 @@ exports.BUGBOT_MESSAGE_IDS = Object.freeze([ 'bugbot.overflow.body', 'bugbot.common.more', 'bugbot.diagnostic.operationFailed', + 'bugbot.diagnostic.findingMutationFailed', 'bugbot.diagnostic.identityUnavailable', 'bugbot.diagnostic.pullRequestCommentsFailed', 'bugbot.diagnostic.reviewThreadsFailed', @@ -42052,6 +42091,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'bugbot.overflow.heading': 'More findings (comment limit)', 'bugbot.overflow.body': Object.freeze({ one: 'There is {count} more finding that was not published as an individual comment. Review locally or in the full diff to see the list.', other: 'There are {count} more findings that were not published as individual comments. Review locally or in the full diff to see the list.' }), 'bugbot.common.more': Object.freeze({ one: 'and {count} more.', other: 'and {count} more.' }), + 'bugbot.diagnostic.findingMutationFailed': 'Finding mutation failed: stage={stage}; operation={operation}; finding={findingId}; affected={affectedCount}; {providerStatus}; provider reference={providerReference}. Completed effects are retained. Correct access/contract errors before retrying; for transient failures restore provider availability, then recheck the same PR. Recheck reconciles existing reviews and threads.', 'bugbot.diagnostic.operationFailed': 'Bugbot could not complete one or more finding mutations.', 'bugbot.diagnostic.identityUnavailable': 'The authenticated Bugbot identity is unavailable.', 'bugbot.diagnostic.pullRequestCommentsFailed': 'Unable to re-read pull request review comments.', @@ -42141,6 +42181,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'bugbot.overflow.heading': 'Más hallazgos (límite de comentarios)', 'bugbot.overflow.body': Object.freeze({ one: 'Hay {count} hallazgo más que no se publicó como comentario individual. Revísalo localmente o en el diff completo para consultar la lista.', many: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.', other: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.' }), 'bugbot.common.more': Object.freeze({ one: 'y {count} más.', many: 'y {count} más.', other: 'y {count} más.' }), + 'bugbot.diagnostic.findingMutationFailed': 'Falló una mutación: fase={stage}; operación={operation}; hallazgo={findingId}; afectados={affectedCount}; {providerStatus}; referencia del proveedor={providerReference}. Se conservan los efectos completados. Corrige acceso/contrato antes de reintentar; para fallos transitorios restaura el proveedor y revisa la misma PR. La revisión reconcilia comentarios e hilos existentes.', 'bugbot.diagnostic.operationFailed': 'Bugbot no pudo completar una o más mutaciones de hallazgos.', 'bugbot.diagnostic.identityUnavailable': 'La identidad autenticada de Bugbot no está disponible.', 'bugbot.diagnostic.pullRequestCommentsFailed': 'No se pudieron volver a leer los comentarios del review del pull request.', @@ -42180,6 +42221,7 @@ async function resolveBugbotCatalog(locale, configuration, resolver) { } function renderBugbotDiagnostic(diagnostic, catalog) { switch (diagnostic.code) { + case 'finding-mutation-failed': return catalog.message('bugbot.diagnostic.findingMutationFailed', { ...diagnostic.mutation }); case 'operation-failed': return catalog.message('bugbot.diagnostic.operationFailed'); case 'identity-unavailable': return catalog.message('bugbot.diagnostic.identityUnavailable'); case 'snapshot-pull-request-comments-failed': return catalog.message('bugbot.diagnostic.pullRequestCommentsFailed'); @@ -42807,6 +42849,10 @@ function renderBugbotStatusCard(projection, catalogOrLocale, links) { facts.push(catalog.message('bugbot.coverage.omitted', { count: source.omittedItems })); if (source.truncatedItems > 0) facts.push(catalog.message('bugbot.coverage.truncated', { count: source.truncatedItems })); + const truncatedIds = source.truncatedItemIds?.slice(0, 50) + .filter(id => /^(?:issue:[0-9]+|review:[A-Za-z0-9_=-]+)$/.test(id) && id.length <= 160); + if (truncatedIds?.length) + facts.push(`truncated-source-ids=${truncatedIds.join(', ')}`); if (source.providerLimitReached) facts.push(catalog.message('bugbot.coverage.providerLimit')); return `- ${source.source}: ${source.status}; ${facts.join(', ')}`; @@ -51205,6 +51251,13 @@ class PullRequestReviewOperationError extends Error { super(buildMessage(operation, context)); this.name = "PullRequestReviewOperationError"; this.operation = operation; + const status = context?.providerStatus; + if (typeof status === 'number' && Number.isInteger(status) && status >= 400 && status <= 599) { + this.providerStatus = status; + } + if (typeof context?.providerReference === 'string' && /^[A-Za-z0-9:-]{1,64}$/.test(context.providerReference)) { + this.providerReference = context.providerReference; + } } } exports.PullRequestReviewOperationError = PullRequestReviewOperationError; @@ -59775,6 +59828,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.applyDetectedFindings = applyDetectedFindings; const mark_findings_resolved_use_case_1 = __nccwpck_require__(96963); const publish_findings_use_case_1 = __nccwpck_require__(88442); +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const pull_request_review_errors_1 = __nccwpck_require__(46445); async function applyDetectedFindings(operation, context, prepared, publicationPorts, resolutionPorts, catalog) { try { @@ -59790,7 +59844,7 @@ async function applyDetectedFindings(operation, context, prepared, publicationPo }); } catch (error) { - const publicationError = error instanceof pull_request_review_errors_1.PullRequestReviewOperationError + const publicationError = error instanceof bugbot_mutation_error_1.BugbotMutationError || error instanceof pull_request_review_errors_1.PullRequestReviewOperationError ? error : new Error("Unable to publish findings."); return [publicationError]; @@ -60460,6 +60514,10 @@ function buildReviewConversationContext(issueComments, commentsByPullRequest, bo omitted, truncated: chronological.filter((entry) => entry.truncated).length, retained: chronological.length, + ...(chronological.some(entry => entry.truncated) ? { + truncatedItemIds: chronological.filter(entry => entry.truncated) + .map(entry => entry.providerId).filter(id => /^(?:issue:[0-9]+|review:[A-Za-z0-9_=-]+)$/.test(id) && id.length <= 160), + } : {}), }; } function appendConversationEntry(entries, author, kind, body, createdAt, providerId) { @@ -61951,6 +62009,7 @@ async function loadBugbotContext(request, ports, resolvedPreflight) { itemsFetched: conversationContext.retained + conversationContext.omitted, omittedItems: conversationContext.omitted, truncatedItems: conversationContext.truncated, + ...(conversationContext.truncatedItemIds ? { truncatedItemIds: conversationContext.truncatedItemIds } : {}), limitReached: conversationContext.omitted > 0 || conversationContext.truncated > 0, }, { @@ -62152,11 +62211,11 @@ Object.defineProperty(exports, "markFindingsResolved", ({ enumerable: true, get: Object.defineProperty(exports, "__esModule", ({ value: true })); exports.markFindingsResolved = markFindingsResolved; -const pull_request_review_errors_1 = __nccwpck_require__(46445); const logging_ports_1 = __nccwpck_require__(6152); const resolve_issue_finding_1 = __nccwpck_require__(35300); const resolve_pull_request_finding_1 = __nccwpck_require__(64567); const review_state_1 = __nccwpck_require__(79200); +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const application_error_1 = __nccwpck_require__(75999); async function markFindingsResolved(param) { const errors = []; @@ -62184,8 +62243,8 @@ async function repairExistingPullRequestFinding(ports, operation, findingId, des try { await ports.pullRequestComments.unresolvePullRequestReviewThread(destination.pullRequestNumber, destination.commentIdentity); } - catch { - addResolutionError(errors, 'pull request'); + catch (cause) { + addResolutionError(errors, 'pull request', findingId, cause); } } } @@ -62199,7 +62258,7 @@ async function resolveIssueIfNeeded(param, findingId, destination, errors) { return; const comment = param.context.issueComments.find(item => item.id === destination.commentId); if (comment?.body == null) { - addResolutionError(errors, 'issue'); + addResolutionError(errors, 'issue', findingId); return; } try { @@ -62210,8 +62269,8 @@ async function resolveIssueIfNeeded(param, findingId, destination, errors) { resolution: param.resolvedFindingResolutions?.get(findingId), }, param.catalog); } - catch { - addResolutionError(errors, 'issue'); + catch (cause) { + addResolutionError(errors, 'issue', findingId, cause); } } async function tryResolvePullRequestFinding(ports, findingId, destination, errors, resolution, catalog) { @@ -62223,17 +62282,14 @@ async function tryResolvePullRequestFinding(ports, findingId, destination, error resolution, }, catalog); } - catch { - addResolutionError(errors, 'pull request'); + catch (cause) { + addResolutionError(errors, 'pull request', findingId, cause); } } -function addResolutionError(errors, destination) { - const cause = destination === 'pull request' - ? new pull_request_review_errors_1.PullRequestReviewOperationError('mark-resolved') - : new Error('Unable to mark an issue finding as resolved.'); - const semanticError = new application_error_1.ApplicationError('provider.unavailable', destination === 'pull request' - ? 'Unable to mark a pull request finding as resolved.' - : 'Unable to mark an issue finding as resolved.', { cause }); +function addResolutionError(errors, destination, findingId, cause) { + const semanticError = destination === 'pull request' + ? (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'resolution', [findingId], 'mark-resolved') + : new application_error_1.ApplicationError('provider.unavailable', 'Unable to mark an issue finding as resolved.'); (0, logging_ports_1.logError)(semanticError); errors.push(semanticError); } @@ -62486,6 +62542,7 @@ function isRecord(value) { */ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.publishFindings = publishFindings; +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const finding_1 = __nccwpck_require__(31011); const publish_issue_finding_comment_1 = __nccwpck_require__(84950); const publish_pr_review_comments_1 = __nccwpck_require__(50352); @@ -62505,14 +62562,24 @@ async function publishFindings(param) { }) : undefined; for (const finding of findings) { - if (operation.target.issueNumber > 0 && !reviewPublisher) { - await (0, publish_issue_finding_comment_1.publishIssueFindingComment)(ports.issueComments, operation.target.issueNumber, finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding), commitSha, catalog); + try { + if (operation.target.issueNumber > 0 && !reviewPublisher) { + await (0, publish_issue_finding_comment_1.publishIssueFindingComment)(ports.issueComments, operation.target.issueNumber, finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding), commitSha, catalog); + } + if (reviewPublisher) { + await reviewPublisher.publish(finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding)); + } } - if (reviewPublisher) { - await reviewPublisher.publish(finding, (0, finding_1.findExistingFindingInfo)(existingByFindingId, finding)); + catch (cause) { + throw (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'publication', [finding.id], 'publish-comments'); } } - await reviewPublisher?.flush(overflowCount, overflowTitles); + try { + await reviewPublisher?.flush(overflowCount, overflowTitles); + } + catch (cause) { + throw (0, bugbot_mutation_error_1.bugbotMutationError)(cause, 'publication', findings.map(finding => finding.id), 'publish-comments'); + } if (operation.target.issueNumber > 0 && !reviewPublisher) { await (0, publish_overflow_comment_1.publishOverflowComment)(ports.issueComments, operation.target.issueNumber, overflowCount, overflowTitles, commitSha, catalog); } @@ -62812,6 +62879,7 @@ async function queryBugbotPartitionFindings(repository, configuration, prompt, t Object.defineProperty(exports, "__esModule", ({ value: true })); exports.reconcileBugbotReviewState = reconcileBugbotReviewState; +const bugbot_mutation_error_1 = __nccwpck_require__(82937); const review_projection_1 = __nccwpck_require__(80859); const bugbot_reconciliation_policy_1 = __nccwpck_require__(78128); const bugbot_provider_projection_policy_1 = __nccwpck_require__(85821); @@ -62843,10 +62911,9 @@ async function reconcileBugbotReviewState(input) { } const snapshot = snapshotResult.snapshot; const diagnostics = [ - ...(input.mutationErrors ?? []).map((error) => ({ - code: 'operation-failed', - operatorMessage: toSafeOperationMessage(error), - })), + ...(input.mutationErrors ?? []).map((error) => error instanceof bugbot_mutation_error_1.BugbotMutationError + ? { code: 'finding-mutation-failed', mutation: error.diagnostic } + : { code: 'operation-failed', operatorMessage: toSafeOperationMessage(error) }), ...(!input.target.trustedAuthorLogin?.trim() ? [{ code: 'identity-unavailable' }] : []), @@ -63370,14 +63437,16 @@ function toPresentationFailure(diagnostic) { const message = (0, bugbot_message_catalog_1.bugbotDiagnosticOperatorMessage)(diagnostic); return { diagnostic, - error: diagnostic.code === 'review-updates-pending' - ? new application_error_1.ApplicationError('workflow.presentation-pending', message, { - recovery: { - id: 'bugbot-review-blocks-pending', - variables: { pendingCount: diagnostic.count }, - }, - }) - : new Error(message), + error: diagnostic.code === 'finding-mutation-failed' + ? new application_error_1.ApplicationError(diagnostic.mutation.errorCode, message) + : diagnostic.code === 'review-updates-pending' + ? new application_error_1.ApplicationError('workflow.presentation-pending', message, { + recovery: { + id: 'bugbot-review-blocks-pending', + variables: { pendingCount: diagnostic.count }, + }, + }) + : new Error(message), }; } function report(projection, reviewUpdates, pendingReviewUpdates, statusCardOperation, errors) { @@ -77612,8 +77681,8 @@ function collectDiagnostic(diagnostics, cause) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestChangesRepository = void 0; +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const logger_1 = __nccwpck_require__(91151); -const pull_request_review_errors_1 = __nccwpck_require__(46445); const github_pagination_policy_1 = __nccwpck_require__(44812); const application_error_1 = __nccwpck_require__(75999); class PullRequestChangesRepository { @@ -77626,7 +77695,7 @@ class PullRequestChangesRepository { } catch (error) { (0, logger_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to read the pull request review diff.')); - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-files'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-files'); } }; this.getBoundedBugbotReviewDiffSnapshot = async (owner, repository, pullNumber, token) => { @@ -77668,7 +77737,7 @@ class PullRequestChangesRepository { } catch (error) { (0, logger_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to read the pull request review diff.')); - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-files'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-files'); } }; /** Head commit SHA of the PR (for creating review). */ @@ -77687,7 +77756,7 @@ class PullRequestChangesRepository { } catch (error) { (0, logger_1.logError)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to read the pull request head SHA.')); - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "get-head-sha"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "get-head-sha"); } }; } @@ -78030,6 +78099,7 @@ function toBugbotPullRequestIdentity(value) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewCommentCommandRepository = void 0; +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const pull_request_review_errors_1 = __nccwpck_require__(46445); const github_pagination_policy_1 = __nccwpck_require__(44812); class PullRequestReviewCommentCommandRepository { @@ -78098,7 +78168,7 @@ class PullRequestReviewCommentCommandRepository { const context = comments.length > 0 ? { failedCount: comments.length, totalCount: comments.length } : undefined; - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "publish-comments", context); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "publish-comments", context); } } async updatePullRequestReview(owner, repository, pullRequestNumber, reviewIdentity, body, token) { @@ -78119,7 +78189,7 @@ class PullRequestReviewCommentCommandRepository { throw new pull_request_review_errors_1.PullRequestReviewOperationError('update-review'); } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'update-review'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'update-review'); } } async updatePullRequestReviewComment(_owner, _repository, commentIdentity, body, token) { @@ -78138,7 +78208,7 @@ class PullRequestReviewCommentCommandRepository { } } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "update-comment"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "update-comment"); } } } @@ -78154,7 +78224,7 @@ exports.PullRequestReviewCommentCommandRepository = PullRequestReviewCommentComm Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewCommentQueryRepository = void 0; -const pull_request_review_errors_1 = __nccwpck_require__(46445); +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const github_pagination_policy_1 = __nccwpck_require__(44812); function toReviewComment(comment) { if (typeof comment.node_id !== "string" || comment.node_id.length === 0) { @@ -78210,7 +78280,7 @@ class PullRequestReviewCommentQueryRepository { return comments; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "list-comments"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "list-comments"); } } async listBugbotPullRequestReviewCommentsBounded(owner, repository, pullRequestNumber, token) { @@ -78253,7 +78323,7 @@ class PullRequestReviewCommentQueryRepository { }; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "list-comments"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "list-comments"); } } async listPullRequestReviews(owner, repository, pullRequestNumber, token) { @@ -78267,7 +78337,7 @@ class PullRequestReviewCommentQueryRepository { return reviews; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-reviews'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-reviews'); } } async getPullRequestReviewCommentBody(owner, repository, _pullRequestNumber, commentId, token) { @@ -78281,13 +78351,37 @@ class PullRequestReviewCommentQueryRepository { return data.body ?? null; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "get-comment"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "get-comment"); } } } exports.PullRequestReviewCommentQueryRepository = PullRequestReviewCommentQueryRepository; +/***/ }), + +/***/ 54354: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPullRequestReviewOperationError = toPullRequestReviewOperationError; +const pull_request_review_errors_1 = __nccwpck_require__(46445); +/** Only safe HTTP metadata crosses the provider boundary; provider prose never does. */ +function toPullRequestReviewOperationError(error, operation, context) { + if (!error || typeof error !== 'object') + return (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, operation, context); + const record = error; + return (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, operation, { + ...context, + ...(typeof record.status === 'number' ? { providerStatus: record.status } : {}), + ...(typeof record.response?.headers?.['x-github-request-id'] === 'string' + ? { providerReference: record.response.headers['x-github-request-id'] } : {}), + }); +} + + /***/ }), /***/ 2307: @@ -78398,6 +78492,7 @@ function nextConnectionCursor(pageInfo, seenCursors) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewThreadRepository = void 0; +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const logger_1 = __nccwpck_require__(91151); const pull_request_review_errors_1 = __nccwpck_require__(46445); const pull_request_review_thread_locator_1 = __nccwpck_require__(2307); @@ -78445,7 +78540,7 @@ class PullRequestReviewThreadRepository { return states; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-threads'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-threads'); } }; this.listBugbotPullRequestReviewThreadStatesBounded = async (owner, repository, pullNumber, token) => { @@ -78514,7 +78609,7 @@ class PullRequestReviewThreadRepository { }; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'list-threads'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'list-threads'); } }; this.resolvePullRequestReviewThread = async (owner, repository, pullNumber, commentIdentity, token) => { @@ -78538,7 +78633,7 @@ class PullRequestReviewThreadRepository { (0, logger_1.logDebugInfo)('Resolved pull request review thread.'); } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'resolve-thread'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'resolve-thread'); } }; this.unresolvePullRequestReviewThread = async (owner, repository, pullNumber, commentIdentity, token) => { @@ -78562,7 +78657,7 @@ class PullRequestReviewThreadRepository { (0, logger_1.logDebugInfo)('Reopened pull request review thread.'); } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, 'unresolve-thread'); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, 'unresolve-thread'); } }; } @@ -78579,7 +78674,7 @@ exports.PullRequestReviewThreadRepository = PullRequestReviewThreadRepository; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PullRequestReviewerRepository = void 0; -const pull_request_review_errors_1 = __nccwpck_require__(46445); +const pull_request_review_error_metadata_1 = __nccwpck_require__(54354); const github_pagination_policy_1 = __nccwpck_require__(44812); const COMPLETED_REVIEW_STATES = new Set([ "APPROVED", @@ -78612,7 +78707,7 @@ class PullRequestReviewerRepository { return [...reviewers.values()]; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "list-reviewers"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "list-reviewers"); } } async addReviewersToPullRequest(owner, repository, pullRequestNumber, reviewers, token) { @@ -78637,7 +78732,7 @@ class PullRequestReviewerRepository { return [...confirmed.values()]; } catch (error) { - throw (0, pull_request_review_errors_1.toPullRequestReviewOperationError)(error, "request-reviewers"); + throw (0, pull_request_review_error_metadata_1.toPullRequestReviewOperationError)(error, "request-reviewers"); } } async listRequestedReviewers(client, parameters) { diff --git a/docs/bugbot/failure-scenarios.mdx b/docs/bugbot/failure-scenarios.mdx index 0264b84cd..c65052edd 100644 --- a/docs/bugbot/failure-scenarios.mdx +++ b/docs/bugbot/failure-scenarios.mdx @@ -51,6 +51,13 @@ description: Diagnose terminal failures across detection, publication, autofix, Bugbot updates historical review blocks in sequential batches of 20, up to 100 per run. If more remain, or a batch fails, the status card and Check report the exact unattempted count and the Action uses `workflow.presentation-pending` for a budget remainder. This does not mean the provider is unavailable or that the diff was only partly analyzed. Re-run `/copilot recheck` after inspecting any separate write failure; successful updates are kept and already-current blocks are skipped. The canonical status card remains the aggregate view throughout. + + Successful publication can precede a later failed marker/thread mutation. The recovery details identify the publication or resolution stage, exact operation, affected finding (plus batch count), HTTP status and provider request reference when available. Missing provider metadata is explicitly unavailable. Existing reviews, comments and durable dismissal intent are retained; a zero completed-publication count on a failed run does not prove that no native review exists. + + Correct permanent credential/access or contract failures before retrying. For a transient outage or rate limit, restore provider availability first, then run `/copilot recheck` on the same PR. The normal fresh provider read reconciles existing comments and threads rather than recreating completed effects. Do not delete a review or install an application dependency merely to clear an unrelated finding-publication failure. + + **Incomplete coverage** remains separate. `human-conversation` lists retained, omitted and truncated counts plus `truncated-source-ids` for exact retained records, such as `issue:6311517518` (a general issue/PR comment ID) or `review:PRRC_50` (an inline comment node ID). Inspect that source from the linked PR/issue, preserve its substantive decisions and reduce only redundant text before rechecking. Source IDs contain no comment text. A mutation retry never upgrades incomplete conversation evidence to complete. + See [Verification commands](/bugbot/verification-commands) and [Security & Operations](/security-operations). diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 7729c582d..2b69c4a1f 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -13,7 +13,7 @@ debt or convert unknown historic intent into a design decision. | `github-communication-experience` | Implemented | English-default, localized, semantic, bounded, and idempotent product messages across GitHub and repository-aware operator surfaces | [Semantic GitHub communication and repository localization](./semantic-github-publication-and-notification.md) + 3 companion | 277 paths · 2026-10-20 | | `release-orchestration` | Implemented | Release and hotfix promotion, publication, bounded merge readiness recovery, and receipt-verified reconciliation without republication | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 59 paths · 2026-10-16 | | `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support; optional manual-only RepoWise analysis with independent CI and verified required-check migration; cached RepoWise exports and bounded full-history secret processing with truthful completeness and resource evidence | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 29 paths · 2026-10-21 | -| `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 59 paths · 2026-10-11 | +| `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 63 paths · 2026-10-21 | | `execution-lifecycle` | Implemented | Shared GitHub Action lifecycle from event admission through durable user-facing results | [Execution admission, queueing, routing, and result publication](./execution-admission-queue-and-publication.md) + 3 companion | 104 paths · 2026-10-21 | | `architecture-quality-hardening` | Implemented | Close verified concurrency, error-contract, context-coupling, fan-out, setup/doctor, and provider-policy risks in dependency order | [Architecture quality and scalability hardening](./architecture-quality-and-scalability-hardening.md) + 1 companion | 72 paths · 2026-10-13 | | `setup-and-doctor` | Implemented | Plan, validate, provision, and audit a repository installation without exposing credentials | [Setup, configuration, credentials, and doctor](./setup-configuration-credentials-and-doctor.md) + 5 companion | 326 paths · 2026-10-20 | @@ -70,12 +70,12 @@ debt or convert unknown historic intent into a design decision. ### `bugbot-review-state-reconciliation` — Bugbot review-state reconciliation - Owner: Copilot maintainers -- Last verified: 2026-10-11 +- Last verified: 2026-10-21 - Specifications: [`specs/bugbot-review-state-reconciliation.md`](./bugbot-review-state-reconciliation.md) - Workflows: [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`src/application/usecases/steps/commit/detect_potential_problems_workflow.ts`](../src/application/usecases/steps/commit/detect_potential_problems_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/reconcile_bugbot_review_state_use_case.ts`](../src/application/usecases/steps/commit/bugbot/reconcile_bugbot_review_state_use_case.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/domain/bugbot/review_state.ts`](../src/domain/bugbot/review_state.ts) · [`src/domain/bugbot/review_projection.ts`](../src/domain/bugbot/review_projection.ts) · [`src/application/policies/bugbot_reconciliation_policy.ts`](../src/application/policies/bugbot_reconciliation_policy.ts) · [`src/application/policies/bugbot_event_ownership_policy.ts`](../src/application/policies/bugbot_event_ownership_policy.ts) · [`src/application/policies/bugbot_result_finding_state_projection_policy.ts`](../src/application/policies/bugbot_result_finding_state_projection_policy.ts) · [`src/application/policies/bugbot_telemetry_projection_policy.ts`](../src/application/policies/bugbot_telemetry_projection_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/bugbot_finding_marker_policy.ts`](../src/application/policies/bugbot_finding_marker_policy.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/copilot_evidence_policy.ts`](../src/application/policies/copilot_evidence_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/load_bugbot_context_use_case.ts`](../src/application/usecases/steps/commit/bugbot/load_bugbot_context_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/schema.ts`](../src/application/usecases/steps/commit/bugbot/schema.ts) · [`src/application/usecases/steps/commit/bugbot/prepare_bugbot_findings_policy.ts`](../src/application/usecases/steps/commit/bugbot/prepare_bugbot_findings_policy.ts) · [`src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts`](../src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts) · [`src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts`](../src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts) · [`src/application/policies/bugbot_status_count_marker_policy.ts`](../src/application/policies/bugbot_status_count_marker_policy.ts) -- Tests: [`src/domain/bugbot/__tests__/review_state.test.ts`](../src/domain/bugbot/__tests__/review_state.test.ts) · [`src/domain/bugbot/__tests__/review_projection.test.ts`](../src/domain/bugbot/__tests__/review_projection.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/schema.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/schema.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/prepare_bugbot_findings.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/prepare_bugbot_findings.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/reconcile_bugbot_review_state_integration.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/reconcile_bugbot_review_state_integration.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_event_ownership_policy.test.ts`](../src/application/policies/__tests__/bugbot_event_ownership_policy.test.ts) · [`src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_finding_marker_policy.test.ts`](../src/application/policies/__tests__/bugbot_finding_marker_policy.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/copilot_evidence_policy.test.ts`](../src/application/policies/__tests__/copilot_evidence_policy.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts) · [`src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/domain/bugbot/review_state.ts`](../src/domain/bugbot/review_state.ts) · [`src/domain/bugbot/review_projection.ts`](../src/domain/bugbot/review_projection.ts) · [`src/application/policies/bugbot_reconciliation_policy.ts`](../src/application/policies/bugbot_reconciliation_policy.ts) · [`src/application/policies/bugbot_event_ownership_policy.ts`](../src/application/policies/bugbot_event_ownership_policy.ts) · [`src/application/policies/bugbot_result_finding_state_projection_policy.ts`](../src/application/policies/bugbot_result_finding_state_projection_policy.ts) · [`src/application/policies/bugbot_telemetry_projection_policy.ts`](../src/application/policies/bugbot_telemetry_projection_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/bugbot_finding_marker_policy.ts`](../src/application/policies/bugbot_finding_marker_policy.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/copilot_evidence_policy.ts`](../src/application/policies/copilot_evidence_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/commit/bugbot/load_bugbot_context_use_case.ts`](../src/application/usecases/steps/commit/bugbot/load_bugbot_context_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/schema.ts`](../src/application/usecases/steps/commit/bugbot/schema.ts) · [`src/application/usecases/steps/commit/bugbot/prepare_bugbot_findings_policy.ts`](../src/application/usecases/steps/commit/bugbot/prepare_bugbot_findings_policy.ts) · [`src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts`](../src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts) · [`src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts`](../src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts) · [`src/application/policies/bugbot_status_count_marker_policy.ts`](../src/application/policies/bugbot_status_count_marker_policy.ts) · [`src/application/errors/bugbot_mutation_error.ts`](../src/application/errors/bugbot_mutation_error.ts) · [`src/data/repository/pull_request/pull_request_review_error_metadata.ts`](../src/data/repository/pull_request/pull_request_review_error_metadata.ts) +- Tests: [`src/domain/bugbot/__tests__/review_state.test.ts`](../src/domain/bugbot/__tests__/review_state.test.ts) · [`src/domain/bugbot/__tests__/review_projection.test.ts`](../src/domain/bugbot/__tests__/review_projection.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/schema.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/schema.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/prepare_bugbot_findings.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/prepare_bugbot_findings.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/reconcile_bugbot_review_state_integration.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/reconcile_bugbot_review_state_integration.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/synchronize_bugbot_review_presentation_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_event_ownership_policy.test.ts`](../src/application/policies/__tests__/bugbot_event_ownership_policy.test.ts) · [`src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_finding_marker_policy.test.ts`](../src/application/policies/__tests__/bugbot_finding_marker_policy.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts) · [`src/application/policies/__tests__/action_summary_policy.test.ts`](../src/application/policies/__tests__/action_summary_policy.test.ts) · [`src/application/policies/__tests__/copilot_evidence_policy.test.ts`](../src/application/policies/__tests__/copilot_evidence_policy.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/publish_resume_use_case.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts`](../src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts) · [`src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/application/errors/__tests__/bugbot_mutation_error.test.ts`](../src/application/errors/__tests__/bugbot_mutation_error.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/finding_mutation_recovery.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/finding_mutation_recovery.test.ts) - User documentation: [`docs/bugbot/detection.mdx`](../docs/bugbot/detection.mdx) · [`docs/bugbot/finding-publication.mdx`](../docs/bugbot/finding-publication.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/bugbot/configuration.mdx`](../docs/bugbot/configuration.mdx) · [`docs/bugbot/how-it-works.mdx`](../docs/bugbot/how-it-works.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/pull-requests/workflow-setup.mdx`](../docs/pull-requests/workflow-setup.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) ### `execution-lifecycle` — Execution admission, queueing, routing, and result publication diff --git a/specs/bugbot-review-state-reconciliation.md b/specs/bugbot-review-state-reconciliation.md index 4bd9be5bf..7c2d2aeb5 100644 --- a/specs/bugbot-review-state-reconciliation.md +++ b/specs/bugbot-review-state-reconciliation.md @@ -1601,3 +1601,72 @@ from one trusted bot-owned complete current-head card; it grants no approval or mutation authority. Marker/reader tests (24 cases) belong exclusively to the comment automation SDD budget. User documentation explains recheck recovery when evidence is absent or stale. + + +### Actionable finding mutation failures (2026-10-21, #646) + +Observed: a native review and inline comment could persist before resolution +failed; the adapter and application discarded the failed operation/HTTP facts, +leaving only `provider.unavailable`. The exact consumer API cause remains unknown. +This amendment MUST preserve incomplete evidence and completed effects while +retaining closed stage (`publication`/`resolution`), adapter operation, first +finding ID, affected batch count, optional HTTP status and safe provider request +reference. Batch diagnosis names its first finding and count, without falsely +claiming which individual item failed inside an atomic provider request. + +Only integer HTTP 400–599 and a 1–64-character alphanumeric/colon/hyphen request +reference are retained. Raw error prose, response bodies, URLs, request headers, +credentials and rejected provider content MUST NOT cross this diagnostic boundary. +Finding IDs use a bounded 200-character identifier alphabet; unavailable or unsafe +IDs are represented explicitly. Existing locally computed fingerprints remain +reconciliation authority; diagnostic IDs grant no mutation permission. + +```text +Publish native review -> attempt durable reconciliation -> failure retains effects +-> safe diagnostic plus independent coverage -> correct cause -> normal recheck +-> fresh provider read -> reuse native finding and retained dismissal +``` + +Text equivalent: partial success is retained and diagnosed; a recheck discovers +existing provider state before continuing, without duplicate review/thread creation +or reversal of a durable human dismissal. There is no new automatic retry or +rollback and no change to freshness, ownership, evidence or mutation ordering. + +The semantic error class MUST preserve authorization, not-found, conflict, +rate-limit, client-contract and transient-provider categories. Recovery details +in the owned status card remain localized with existing English/Spanish catalogs. +Example: “Finding mutation failed: stage=resolution; operation=resolve-thread; +finding=backend/Dockerfile:20:healthcheck-requires-curl; affected=1; http-503; +provider reference=AB12:34CD. Completed effects are retained.” Correct access and +contract failures before retrying; restore availability for transient failures, +then recheck the same PR. Existing pending, attention and successful cards remain. +No new comment stream, permission, configuration input or durable marker schema. +Absent metadata remains unavailable; no inference of a specific provider cause. + +Conversation truncation MUST remain independently partial. Coverage can attach +up to 50 bounded provider identities for retained truncated sources (`issue:` +or `review:`), never comment text or arbitrary links. The aggregate shows +these identities under Incomplete coverage, separately from Recovery details. +A failed mutation cannot turn partial coverage into complete, and a coverage +limitation cannot conceal the semantic mutation failure. Existing projection +precedence (partial coverage may yield a partial projection with failed Result) +is retained. Old coverage without identities remains compatible and unverified. + +Adapters own safe provider metadata; application errors own the credential-free +mutation diagnostic; publication/resolution use cases bind finding identity; +reconciliation and catalog presentation consume typed facts. Domain coverage +adds optional source identities only. Typecheck, architecture, localization and +fault-injection regressions enforce these boundaries; SDKs remain outside policy. + +The additional budget is **19 distinct cases**: eleven HTTP/classification/privacy +and error-preservation cases, two publication-stage identity/batch cases, one real +publication-to-failed-resolution-to-retry integration, two locale/projection and +independent-coverage cases, two exact-source/privacy cases and one adapter case. +Existing global coverage/quality thresholds and all prior state/replay tests +remain required. Acceptance proves a persisted native review survives a subsequent +503, one recheck produces no duplicate native review/thread, dismissal bytes are +retained, and diagnostics distinguish that error from truncated conversation. +Documentation: Bugbot failure scenarios. Upgrade the Action and recheck the same +PR after recovery; rollback only loses richer diagnostics. Definition of Done: +all package, documentation, catalog and existing architecture gates pass; no live +consumer recovery is claimed by deterministic fixtures. diff --git a/specs/catalog.json b/specs/catalog.json index d2a52a9bb..f70d81690 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -440,7 +440,7 @@ "status": "implemented", "scope": "Reconcile review snapshots, findings, threads, comments, and check conclusions", "owner": "Copilot maintainers", - "lastVerified": "2026-10-11", + "lastVerified": "2026-10-21", "specs": [ "specs/bugbot-review-state-reconciliation.md" ], @@ -476,7 +476,9 @@ "src/application/usecases/steps/commit/bugbot/prepare_bugbot_findings_policy.ts", "src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts", "src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts", - "src/application/policies/bugbot_status_count_marker_policy.ts" + "src/application/policies/bugbot_status_count_marker_policy.ts", + "src/application/errors/bugbot_mutation_error.ts", + "src/data/repository/pull_request/pull_request_review_error_metadata.ts" ], "tests": [ "src/domain/bugbot/__tests__/review_state.test.ts", @@ -500,7 +502,9 @@ "src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts", "src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts", "src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts", - "src/tooling/__tests__/validate_workflow_contract.test.ts" + "src/tooling/__tests__/validate_workflow_contract.test.ts", + "src/application/errors/__tests__/bugbot_mutation_error.test.ts", + "src/application/usecases/steps/commit/bugbot/__tests__/finding_mutation_recovery.test.ts" ], "documentation": [ "docs/bugbot/detection.mdx", diff --git a/src/application/contracts/bugbot_reconciliation.ts b/src/application/contracts/bugbot_reconciliation.ts index 192ac3f24..3fd4a673c 100644 --- a/src/application/contracts/bugbot_reconciliation.ts +++ b/src/application/contracts/bugbot_reconciliation.ts @@ -1,3 +1,4 @@ +import type { BugbotMutationDiagnostic } from '../errors/bugbot_mutation_error'; import type { BugbotIssueComment } from '../ports/bugbot_issue_read_ports'; import type { BugbotReviewNavigation } from '../ports/bugbot_review_navigation_ports'; import type { @@ -60,6 +61,7 @@ export type BugbotReconciliationSnapshotResult = }; export type BugbotPresentationDiagnostic = + | { readonly code: 'finding-mutation-failed'; readonly mutation: BugbotMutationDiagnostic } | { readonly code: 'operation-failed'; readonly operatorMessage: string } | { readonly code: 'identity-unavailable' } | { readonly code: 'snapshot-pull-request-comments-failed' } diff --git a/src/application/errors/__tests__/bugbot_mutation_error.test.ts b/src/application/errors/__tests__/bugbot_mutation_error.test.ts new file mode 100644 index 000000000..a85091301 --- /dev/null +++ b/src/application/errors/__tests__/bugbot_mutation_error.test.ts @@ -0,0 +1,34 @@ +import { BugbotMutationError, bugbotMutationError } from '../bugbot_mutation_error'; +import { PullRequestReviewOperationError, toPullRequestReviewOperationError } from '../../ports/pull_request_review_errors'; + +it.each([ + [401, 'authorization.credential-invalid'], [403, 'authorization.denied'], [404, 'provider.not-found'], + [409, 'provider.conflict'], [422, 'provider.contract-invalid'], [429, 'provider.rate-limited'], + [503, 'provider.unavailable'], +])('preserves safe HTTP %s metadata and classifies recovery as %s', (status, code) => { + const cause = new PullRequestReviewOperationError('resolve-thread', { providerStatus: status, providerReference: 'AB12:34CD' }); + const error = bugbotMutationError(cause, 'resolution', ['backend/Dockerfile:20:healthcheck-requires-curl'], 'mark-resolved'); + expect(error).toMatchObject({ code, diagnostic: { stage: 'resolution', operation: 'resolve-thread', + providerStatus: `http-${status}`, providerReference: 'AB12:34CD' } }); + expect(JSON.stringify(error)).not.toMatch(/private-/); +}); + +it('drops malformed provider status/reference and unsafe finding markup', () => { + const cause = new PullRequestReviewOperationError('publish-comments', { providerStatus: 400.5, providerReference: '@team\nsecret' }); + const error = bugbotMutationError(cause, 'publication', ['@team