diff --git a/build/api/index.js b/build/api/index.js index 420ad67f7..f9e63f5e1 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -7,12 +7,13 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = void 0; +exports.ApplicationError = exports.APPLICATION_ERROR_RECOVERY_IDS = exports.APPLICATION_ERROR_METADATA = void 0; exports.toApplicationError = toApplicationError; const application_error_1 = __nccwpck_require__(7790); const application_error_context_1 = __nccwpck_require__(4034); var application_error_2 = __nccwpck_require__(7790); Object.defineProperty(exports, "APPLICATION_ERROR_METADATA", ({ enumerable: true, get: function () { return application_error_2.APPLICATION_ERROR_METADATA; } })); +Object.defineProperty(exports, "APPLICATION_ERROR_RECOVERY_IDS", ({ enumerable: true, get: function () { return application_error_2.APPLICATION_ERROR_RECOVERY_IDS; } })); /** Creates a semantic error and owns correlation identity outside the pure model. */ class ApplicationError extends application_error_1.ApplicationError { constructor(code, message, options = {}) { @@ -4528,8 +4529,16 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function ( }; var _ApplicationError_cause; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = void 0; +exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = exports.APPLICATION_ERROR_RECOVERY_IDS = void 0; exports.isApplicationErrorCorrelationId = isApplicationErrorCorrelationId; +exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ + 'pull-request-link-restored', + 'pull-request-link-base-retained', + 'pull-request-link-reference-retained', + 'pull-request-link-base-and-reference-retained', + 'managed-branch-enrichment-failed', + 'inactivity-explanation-failed', +]); const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; exports.APPLICATION_ERROR_METADATA = { @@ -4677,10 +4686,11 @@ class ApplicationError extends Error { this.code = code; this.kind = metadata.kind; this.retryable = options.retryable ?? metadata.retryable; - this.impact = options.impact ?? metadata.impact; - this.action = options.action ?? metadata.action; - this.retainedState = options.retainedState ?? metadata.retainedState; + this.impact = metadata.impact; + this.action = metadata.action; + this.retainedState = metadata.retainedState; this.correlationId = correlationId; + this.recovery = normalizeApplicationErrorRecovery(options.recovery); __classPrivateFieldSet(this, _ApplicationError_cause, options.cause, "f"); } toJSON() { @@ -4694,11 +4704,49 @@ class ApplicationError extends Error { action: this.action, retainedState: this.retainedState, correlationId: this.correlationId, + ...(this.recovery ? { recovery: this.recovery } : {}), }; } } exports.ApplicationError = ApplicationError; _ApplicationError_cause = new WeakMap(); +const RECOVERY_VARIABLE_KEYS = Object.freeze({ + 'pull-request-link-restored': Object.freeze([]), + 'pull-request-link-base-retained': Object.freeze([]), + 'pull-request-link-reference-retained': Object.freeze([]), + 'pull-request-link-base-and-reference-retained': Object.freeze([]), + 'managed-branch-enrichment-failed': Object.freeze(['branchName']), + 'inactivity-explanation-failed': Object.freeze(['issueNumber']), +}); +function normalizeApplicationErrorRecovery(recovery) { + if (!recovery) + return undefined; + if (!exports.APPLICATION_ERROR_RECOVERY_IDS.includes(recovery.id)) { + throw new TypeError('Application error recovery ID is invalid.'); + } + const variables = recovery.variables; + const actualKeys = Object.keys(variables).sort(); + const expectedKeys = [...RECOVERY_VARIABLE_KEYS[recovery.id]].sort(); + if (actualKeys.length !== expectedKeys.length + || actualKeys.some((key, index) => key !== expectedKeys[index])) { + throw new TypeError(`Application error recovery variables are invalid for ${recovery.id}.`); + } + if (recovery.id === 'managed-branch-enrichment-failed' + && (typeof variables.branchName !== 'string' + || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/u.test(variables.branchName))) { + throw new TypeError('Application error recovery branch name is invalid.'); + } + if (recovery.id === 'inactivity-explanation-failed' + && (typeof variables.issueNumber !== 'number' + || !Number.isSafeInteger(variables.issueNumber) + || variables.issueNumber < 1)) { + throw new TypeError('Application error recovery issue number is invalid.'); + } + return Object.freeze({ + id: recovery.id, + variables: Object.freeze({ ...variables }), + }); +} /***/ }), @@ -5338,7 +5386,7 @@ exports.renderCatalogMessage = renderCatalogMessage; exports.catalogPlaceholders = catalogPlaceholders; exports.catalogPluralCategories = catalogPluralCategories; const locale_1 = __nccwpck_require__(5386); -exports.MESSAGE_CATALOG_VERSION = '1'; +exports.MESSAGE_CATALOG_VERSION = '2'; exports.CATALOG_PLURAL_CATEGORIES = Object.freeze([ 'zero', 'one', diff --git a/build/api/src/application/errors/application_error.d.ts b/build/api/src/application/errors/application_error.d.ts index fc4ed7691..fab90f64e 100644 --- a/build/api/src/application/errors/application_error.d.ts +++ b/build/api/src/application/errors/application_error.d.ts @@ -1,5 +1,5 @@ import { ApplicationError as SemanticApplicationError, type ApplicationErrorCode, type SemanticApplicationErrorOptions } from '../../data/model/application_error'; -export { APPLICATION_ERROR_METADATA, type ApplicationErrorCode, type ApplicationErrorKind, type ApplicationErrorPublicRecord, } from '../../data/model/application_error'; +export { APPLICATION_ERROR_METADATA, APPLICATION_ERROR_RECOVERY_IDS, type ApplicationErrorCode, type ApplicationErrorKind, type ApplicationErrorPublicRecord, type ApplicationErrorRecovery, type ApplicationErrorRecoveryId, } from '../../data/model/application_error'; export interface ApplicationErrorOptions extends Omit { readonly correlationId?: string; } diff --git a/build/api/src/data/model/application_error.d.ts b/build/api/src/data/model/application_error.d.ts index 26a532e14..ada47db9e 100644 --- a/build/api/src/data/model/application_error.d.ts +++ b/build/api/src/data/model/application_error.d.ts @@ -1,5 +1,25 @@ export type ApplicationErrorKind = 'configuration' | 'authorization' | 'provider' | 'agent' | 'validation' | 'workflow' | 'unknown'; export type ApplicationErrorCode = 'configuration.invalid' | 'configuration.unsupported' | 'authorization.denied' | 'authorization.credential-invalid' | 'provider.not-found' | 'provider.conflict' | 'provider.rate-limited' | 'provider.unavailable' | 'provider.contract-invalid' | 'agent.policy-rejected' | 'agent.failed' | 'locale.output-invalid' | 'locale.translation-failed' | 'validation.invalid-input' | 'workflow.invalid-event' | 'workflow.stale' | 'workflow.cancelled' | 'workflow.failed' | 'timeout' | 'unexpected'; +export declare const APPLICATION_ERROR_RECOVERY_IDS: readonly ["pull-request-link-restored", "pull-request-link-base-retained", "pull-request-link-reference-retained", "pull-request-link-base-and-reference-retained", "managed-branch-enrichment-failed", "inactivity-explanation-failed"]; +export type ApplicationErrorRecoveryId = typeof APPLICATION_ERROR_RECOVERY_IDS[number]; +interface ApplicationErrorRecoveryVariables { + readonly 'pull-request-link-restored': Readonly>; + readonly 'pull-request-link-base-retained': Readonly>; + readonly 'pull-request-link-reference-retained': Readonly>; + readonly 'pull-request-link-base-and-reference-retained': Readonly>; + readonly 'managed-branch-enrichment-failed': Readonly<{ + branchName: string; + }>; + readonly 'inactivity-explanation-failed': Readonly<{ + issueNumber: number; + }>; +} +export type ApplicationErrorRecovery = { + readonly [Id in ApplicationErrorRecoveryId]: Readonly<{ + id: Id; + variables: ApplicationErrorRecoveryVariables[Id]; + }>; +}[ApplicationErrorRecoveryId]; interface ApplicationErrorMetadata { readonly kind: ApplicationErrorKind; readonly retryable: boolean; @@ -10,9 +30,7 @@ interface ApplicationErrorMetadata { export declare const APPLICATION_ERROR_METADATA: Readonly>; export interface SemanticApplicationErrorOptions { readonly retryable?: boolean; - readonly impact?: string; - readonly action?: string; - readonly retainedState?: string; + readonly recovery?: ApplicationErrorRecovery; readonly correlationId: string; readonly cause?: unknown; } @@ -26,6 +44,7 @@ export interface ApplicationErrorPublicRecord { readonly action: string; readonly retainedState: string; readonly correlationId: string; + readonly recovery?: ApplicationErrorRecovery; } export declare function isApplicationErrorCorrelationId(value: string): boolean; /** Semantic error contract whose public fields are safe to serialize and present. */ @@ -39,6 +58,7 @@ export declare class ApplicationError extends Error { readonly action: string; readonly retainedState: string; readonly correlationId: string; + readonly recovery?: ApplicationErrorRecovery; constructor(code: ApplicationErrorCode, message: string, options: SemanticApplicationErrorOptions); toJSON(): ApplicationErrorPublicRecord; } diff --git a/build/api/src/domain/message_catalog.d.ts b/build/api/src/domain/message_catalog.d.ts index 56fbcb877..c36ea50a7 100644 --- a/build/api/src/domain/message_catalog.d.ts +++ b/build/api/src/domain/message_catalog.d.ts @@ -1,4 +1,4 @@ -export declare const MESSAGE_CATALOG_VERSION = "1"; +export declare const MESSAGE_CATALOG_VERSION = "2"; export type CatalogResolutionSource = 'exact' | 'base' | 'dynamic' | 'fallback'; export type CatalogFallbackReason = 'dynamic-provider-unavailable' | 'dynamic-response-invalid' | 'dynamic-request-failed'; export declare const CATALOG_PLURAL_CATEGORIES: readonly ["zero", "one", "two", "few", "many", "other"]; diff --git a/build/cli/index.js b/build/cli/index.js index 5dfd64bfb..b37c87333 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -2,1214 +2,6 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 94361: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.issueCommand = issueCommand; -exports.issue = issue; -const os = __importStar(__nccwpck_require__(22037)); -const utils_1 = __nccwpck_require__(15206); -/** - * Issues a command to the GitHub Actions runner - * - * @param command - The command name to issue - * @param properties - Additional properties for the command (key-value pairs) - * @param message - The message to include with the command - * @remarks - * This function outputs a specially formatted string to stdout that the Actions - * runner interprets as a command. These commands can control workflow behavior, - * set outputs, create annotations, mask values, and more. - * - * Command Format: - * ::name key=value,key=value::message - * - * @example - * ```typescript - * // Issue a warning annotation - * issueCommand('warning', {}, 'This is a warning message'); - * // Output: ::warning::This is a warning message - * - * // Set an environment variable - * issueCommand('set-env', { name: 'MY_VAR' }, 'some value'); - * // Output: ::set-env name=MY_VAR::some value - * - * // Add a secret mask - * issueCommand('add-mask', {}, 'secretValue123'); - * // Output: ::add-mask::secretValue123 - * ``` - * - * @internal - * This is an internal utility function that powers the public API functions - * such as setSecret, warning, error, and exportVariable. - */ -function issueCommand(command, properties, message) { - const cmd = new Command(command, properties, message); - process.stdout.write(cmd.toString() + os.EOL); -} -function issue(name, message = '') { - issueCommand(name, {}, message); -} -const CMD_STRING = '::'; -class Command { - constructor(command, properties, message) { - if (!command) { - command = 'missing.command'; - } - this.command = command; - this.properties = properties; - this.message = message; - } - toString() { - let cmdStr = CMD_STRING + this.command; - if (this.properties && Object.keys(this.properties).length > 0) { - cmdStr += ' '; - let first = true; - for (const key in this.properties) { - if (this.properties.hasOwnProperty(key)) { - const val = this.properties[key]; - if (val) { - if (first) { - first = false; - } - else { - cmdStr += ','; - } - cmdStr += `${key}=${escapeProperty(val)}`; - } - } - } - } - cmdStr += `${CMD_STRING}${escapeData(this.message)}`; - return cmdStr; - } -} -function escapeData(s) { - return (0, utils_1.toCommandValue)(s) - .replace(/%/g, '%25') - .replace(/\r/g, '%0D') - .replace(/\n/g, '%0A'); -} -function escapeProperty(s) { - return (0, utils_1.toCommandValue)(s) - .replace(/%/g, '%25') - .replace(/\r/g, '%0D') - .replace(/\n/g, '%0A') - .replace(/:/g, '%3A') - .replace(/,/g, '%2C'); -} -//# sourceMappingURL=command.js.map - -/***/ }), - -/***/ 75855: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.platform = exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = exports.markdownSummary = exports.summary = exports.ExitCode = void 0; -exports.exportVariable = exportVariable; -exports.setSecret = setSecret; -exports.addPath = addPath; -exports.getInput = getInput; -exports.getMultilineInput = getMultilineInput; -exports.getBooleanInput = getBooleanInput; -exports.setOutput = setOutput; -exports.setCommandEcho = setCommandEcho; -exports.setFailed = setFailed; -exports.isDebug = isDebug; -exports.debug = debug; -exports.error = error; -exports.warning = warning; -exports.notice = notice; -exports.info = info; -exports.startGroup = startGroup; -exports.endGroup = endGroup; -exports.group = group; -exports.saveState = saveState; -exports.getState = getState; -exports.getIDToken = getIDToken; -const command_1 = __nccwpck_require__(94361); -const file_command_1 = __nccwpck_require__(31618); -const utils_1 = __nccwpck_require__(15206); -const os = __importStar(__nccwpck_require__(22037)); -const path = __importStar(__nccwpck_require__(71017)); -const oidc_utils_1 = __nccwpck_require__(88247); -/** - * The code to exit an action - */ -var ExitCode; -(function (ExitCode) { - /** - * A code indicating that the action was successful - */ - ExitCode[ExitCode["Success"] = 0] = "Success"; - /** - * A code indicating that the action was a failure - */ - ExitCode[ExitCode["Failure"] = 1] = "Failure"; -})(ExitCode || (exports.ExitCode = ExitCode = {})); -//----------------------------------------------------------------------- -// Variables -//----------------------------------------------------------------------- -/** - * Sets env variable for this action and future actions in the job - * @param name the name of the variable to set - * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function exportVariable(name, val) { - const convertedVal = (0, utils_1.toCommandValue)(val); - process.env[name] = convertedVal; - const filePath = process.env['GITHUB_ENV'] || ''; - if (filePath) { - return (0, file_command_1.issueFileCommand)('ENV', (0, file_command_1.prepareKeyValueMessage)(name, val)); - } - (0, command_1.issueCommand)('set-env', { name }, convertedVal); -} -/** - * Registers a secret which will get masked from logs - * - * @param secret - Value of the secret to be masked - * @remarks - * This function instructs the Actions runner to mask the specified value in any - * logs produced during the workflow run. Once registered, the secret value will - * be replaced with asterisks (***) whenever it appears in console output, logs, - * or error messages. - * - * This is useful for protecting sensitive information such as: - * - API keys - * - Access tokens - * - Authentication credentials - * - URL parameters containing signatures (SAS tokens) - * - * Note that masking only affects future logs; any previous appearances of the - * secret in logs before calling this function will remain unmasked. - * - * @example - * ```typescript - * // Register an API token as a secret - * const apiToken = "abc123xyz456"; - * setSecret(apiToken); - * - * // Now any logs containing this value will show *** instead - * console.log(`Using token: ${apiToken}`); // Outputs: "Using token: ***" - * ``` - */ -function setSecret(secret) { - (0, command_1.issueCommand)('add-mask', {}, secret); -} -/** - * Prepends inputPath to the PATH (for this action and future actions) - * @param inputPath - */ -function addPath(inputPath) { - const filePath = process.env['GITHUB_PATH'] || ''; - if (filePath) { - (0, file_command_1.issueFileCommand)('PATH', inputPath); - } - else { - (0, command_1.issueCommand)('add-path', {}, inputPath); - } - process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; -} -/** - * Gets the value of an input. - * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. - * Returns an empty string if the value is not defined. - * - * @param name name of the input to get - * @param options optional. See InputOptions. - * @returns string - */ -function getInput(name, options) { - const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; - if (options && options.required && !val) { - throw new Error(`Input required and not supplied: ${name}`); - } - if (options && options.trimWhitespace === false) { - return val; - } - return val.trim(); -} -/** - * Gets the values of an multiline input. Each value is also trimmed. - * - * @param name name of the input to get - * @param options optional. See InputOptions. - * @returns string[] - * - */ -function getMultilineInput(name, options) { - const inputs = getInput(name, options) - .split('\n') - .filter(x => x !== ''); - if (options && options.trimWhitespace === false) { - return inputs; - } - return inputs.map(input => input.trim()); -} -/** - * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. - * Support boolean input list: `true | True | TRUE | false | False | FALSE` . - * The return value is also in boolean type. - * ref: https://yaml.org/spec/1.2/spec.html#id2804923 - * - * @param name name of the input to get - * @param options optional. See InputOptions. - * @returns boolean - */ -function getBooleanInput(name, options) { - const trueValue = ['true', 'True', 'TRUE']; - const falseValue = ['false', 'False', 'FALSE']; - const val = getInput(name, options); - if (trueValue.includes(val)) - return true; - if (falseValue.includes(val)) - return false; - throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + - `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); -} -/** - * Sets the value of an output. - * - * @param name name of the output to set - * @param value value to store. Non-string values will be converted to a string via JSON.stringify - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function setOutput(name, value) { - const filePath = process.env['GITHUB_OUTPUT'] || ''; - if (filePath) { - return (0, file_command_1.issueFileCommand)('OUTPUT', (0, file_command_1.prepareKeyValueMessage)(name, value)); - } - process.stdout.write(os.EOL); - (0, command_1.issueCommand)('set-output', { name }, (0, utils_1.toCommandValue)(value)); -} -/** - * Enables or disables the echoing of commands into stdout for the rest of the step. - * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. - * - */ -function setCommandEcho(enabled) { - (0, command_1.issue)('echo', enabled ? 'on' : 'off'); -} -//----------------------------------------------------------------------- -// Results -//----------------------------------------------------------------------- -/** - * Sets the action status to failed. - * When the action exits it will be with an exit code of 1 - * @param message add error issue message - */ -function setFailed(message) { - process.exitCode = ExitCode.Failure; - error(message); -} -//----------------------------------------------------------------------- -// Logging Commands -//----------------------------------------------------------------------- -/** - * Gets whether Actions Step Debug is on or not - */ -function isDebug() { - return process.env['RUNNER_DEBUG'] === '1'; -} -/** - * Writes debug message to user log - * @param message debug message - */ -function debug(message) { - (0, command_1.issueCommand)('debug', {}, message); -} -/** - * Adds an error issue - * @param message error issue message. Errors will be converted to string via toString() - * @param properties optional properties to add to the annotation. - */ -function error(message, properties = {}) { - (0, command_1.issueCommand)('error', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); -} -/** - * Adds a warning issue - * @param message warning issue message. Errors will be converted to string via toString() - * @param properties optional properties to add to the annotation. - */ -function warning(message, properties = {}) { - (0, command_1.issueCommand)('warning', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); -} -/** - * Adds a notice issue - * @param message notice issue message. Errors will be converted to string via toString() - * @param properties optional properties to add to the annotation. - */ -function notice(message, properties = {}) { - (0, command_1.issueCommand)('notice', (0, utils_1.toCommandProperties)(properties), message instanceof Error ? message.toString() : message); -} -/** - * Writes info to log with console.log. - * @param message info message - */ -function info(message) { - process.stdout.write(message + os.EOL); -} -/** - * Begin an output group. - * - * Output until the next `groupEnd` will be foldable in this group - * - * @param name The name of the output group - */ -function startGroup(name) { - (0, command_1.issue)('group', name); -} -/** - * End an output group. - */ -function endGroup() { - (0, command_1.issue)('endgroup'); -} -/** - * Wrap an asynchronous function call in a group. - * - * Returns the same type as the function itself. - * - * @param name The name of the group - * @param fn The function to wrap in the group - */ -function group(name, fn) { - return __awaiter(this, void 0, void 0, function* () { - startGroup(name); - let result; - try { - result = yield fn(); - } - finally { - endGroup(); - } - return result; - }); -} -//----------------------------------------------------------------------- -// Wrapper action state -//----------------------------------------------------------------------- -/** - * Saves state for current action, the state can only be retrieved by this action's post job execution. - * - * @param name name of the state to store - * @param value value to store. Non-string values will be converted to a string via JSON.stringify - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function saveState(name, value) { - const filePath = process.env['GITHUB_STATE'] || ''; - if (filePath) { - return (0, file_command_1.issueFileCommand)('STATE', (0, file_command_1.prepareKeyValueMessage)(name, value)); - } - (0, command_1.issueCommand)('save-state', { name }, (0, utils_1.toCommandValue)(value)); -} -/** - * Gets the value of an state set by this action's main execution. - * - * @param name name of the state to get - * @returns string - */ -function getState(name) { - return process.env[`STATE_${name}`] || ''; -} -function getIDToken(aud) { - return __awaiter(this, void 0, void 0, function* () { - return yield oidc_utils_1.OidcClient.getIDToken(aud); - }); -} -/** - * Summary exports - */ -var summary_1 = __nccwpck_require__(91785); -Object.defineProperty(exports, "summary", ({ enumerable: true, get: function () { return summary_1.summary; } })); -/** - * @deprecated use core.summary - */ -var summary_2 = __nccwpck_require__(91785); -Object.defineProperty(exports, "markdownSummary", ({ enumerable: true, get: function () { return summary_2.markdownSummary; } })); -/** - * Path exports - */ -var path_utils_1 = __nccwpck_require__(14520); -Object.defineProperty(exports, "toPosixPath", ({ enumerable: true, get: function () { return path_utils_1.toPosixPath; } })); -Object.defineProperty(exports, "toWin32Path", ({ enumerable: true, get: function () { return path_utils_1.toWin32Path; } })); -Object.defineProperty(exports, "toPlatformPath", ({ enumerable: true, get: function () { return path_utils_1.toPlatformPath; } })); -/** - * Platform utilities exports - */ -exports.platform = __importStar(__nccwpck_require__(34525)); -//# sourceMappingURL=core.js.map - -/***/ }), - -/***/ 31618: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -// For internal use, subject to change. -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.issueFileCommand = issueFileCommand; -exports.prepareKeyValueMessage = prepareKeyValueMessage; -// We use any as a valid input type -/* eslint-disable @typescript-eslint/no-explicit-any */ -const crypto = __importStar(__nccwpck_require__(6113)); -const fs = __importStar(__nccwpck_require__(57147)); -const os = __importStar(__nccwpck_require__(22037)); -const utils_1 = __nccwpck_require__(15206); -function issueFileCommand(command, message) { - const filePath = process.env[`GITHUB_${command}`]; - if (!filePath) { - throw new Error(`Unable to find environment variable for file command ${command}`); - } - if (!fs.existsSync(filePath)) { - throw new Error(`Missing file at path: ${filePath}`); - } - fs.appendFileSync(filePath, `${(0, utils_1.toCommandValue)(message)}${os.EOL}`, { - encoding: 'utf8' - }); -} -function prepareKeyValueMessage(key, value) { - const delimiter = `ghadelimiter_${crypto.randomUUID()}`; - const convertedValue = (0, utils_1.toCommandValue)(value); - // These should realistically never happen, but just in case someone finds a - // way to exploit uuid generation let's not allow keys or values that contain - // the delimiter. - if (key.includes(delimiter)) { - throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); - } - if (convertedValue.includes(delimiter)) { - throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); - } - return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; -} -//# sourceMappingURL=file-command.js.map - -/***/ }), - -/***/ 88247: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.OidcClient = void 0; -const http_client_1 = __nccwpck_require__(75784); -const auth_1 = __nccwpck_require__(57281); -const core_1 = __nccwpck_require__(75855); -class OidcClient { - static createHttpClient(allowRetry = true, maxRetry = 10) { - const requestOptions = { - allowRetries: allowRetry, - maxRetries: maxRetry - }; - return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions); - } - static getRequestToken() { - const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; - if (!token) { - throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'); - } - return token; - } - static getIDTokenUrl() { - const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; - if (!runtimeUrl) { - throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable'); - } - return runtimeUrl; - } - static getCall(id_token_url) { - return __awaiter(this, void 0, void 0, function* () { - var _a; - const httpclient = OidcClient.createHttpClient(); - const res = yield httpclient - .getJson(id_token_url) - .catch(error => { - throw new Error(`Failed to get ID Token. \n - Error Code : ${error.statusCode}\n - Error Message: ${error.message}`); - }); - const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; - if (!id_token) { - throw new Error('Response json body do not have ID Token field'); - } - return id_token; - }); - } - static getIDToken(audience) { - return __awaiter(this, void 0, void 0, function* () { - try { - // New ID Token is requested from action service - let id_token_url = OidcClient.getIDTokenUrl(); - if (audience) { - const encodedAudience = encodeURIComponent(audience); - id_token_url = `${id_token_url}&audience=${encodedAudience}`; - } - (0, core_1.debug)(`ID token url is ${id_token_url}`); - const id_token = yield OidcClient.getCall(id_token_url); - (0, core_1.setSecret)(id_token); - return id_token; - } - catch (error) { - throw new Error(`Error message: ${error.message}`); - } - }); - } -} -exports.OidcClient = OidcClient; -//# sourceMappingURL=oidc-utils.js.map - -/***/ }), - -/***/ 14520: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.toPosixPath = toPosixPath; -exports.toWin32Path = toWin32Path; -exports.toPlatformPath = toPlatformPath; -const path = __importStar(__nccwpck_require__(71017)); -/** - * toPosixPath converts the given path to the posix form. On Windows, \\ will be - * replaced with /. - * - * @param pth. Path to transform. - * @return string Posix path. - */ -function toPosixPath(pth) { - return pth.replace(/[\\]/g, '/'); -} -/** - * toWin32Path converts the given path to the win32 form. On Linux, / will be - * replaced with \\. - * - * @param pth. Path to transform. - * @return string Win32 path. - */ -function toWin32Path(pth) { - return pth.replace(/[/]/g, '\\'); -} -/** - * toPlatformPath converts the given path to a platform-specific path. It does - * this by replacing instances of / and \ with the platform-specific path - * separator. - * - * @param pth The path to platformize. - * @return string The platform-specific path. - */ -function toPlatformPath(pth) { - return pth.replace(/[/\\]/g, path.sep); -} -//# sourceMappingURL=path-utils.js.map - -/***/ }), - -/***/ 34525: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.isLinux = exports.isMacOS = exports.isWindows = exports.arch = exports.platform = void 0; -exports.getDetails = getDetails; -const os_1 = __importDefault(__nccwpck_require__(22037)); -const exec = __importStar(__nccwpck_require__(18538)); -const getWindowsInfo = () => __awaiter(void 0, void 0, void 0, function* () { - const { stdout: version } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Version"', undefined, { - silent: true - }); - const { stdout: name } = yield exec.getExecOutput('powershell -command "(Get-CimInstance -ClassName Win32_OperatingSystem).Caption"', undefined, { - silent: true - }); - return { - name: name.trim(), - version: version.trim() - }; -}); -const getMacOsInfo = () => __awaiter(void 0, void 0, void 0, function* () { - var _a, _b, _c, _d; - const { stdout } = yield exec.getExecOutput('sw_vers', undefined, { - silent: true - }); - const version = (_b = (_a = stdout.match(/ProductVersion:\s*(.+)/)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ''; - const name = (_d = (_c = stdout.match(/ProductName:\s*(.+)/)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : ''; - return { - name, - version - }; -}); -const getLinuxInfo = () => __awaiter(void 0, void 0, void 0, function* () { - const { stdout } = yield exec.getExecOutput('lsb_release', ['-i', '-r', '-s'], { - silent: true - }); - const [name, version] = stdout.trim().split('\n'); - return { - name, - version - }; -}); -exports.platform = os_1.default.platform(); -exports.arch = os_1.default.arch(); -exports.isWindows = exports.platform === 'win32'; -exports.isMacOS = exports.platform === 'darwin'; -exports.isLinux = exports.platform === 'linux'; -function getDetails() { - return __awaiter(this, void 0, void 0, function* () { - return Object.assign(Object.assign({}, (yield (exports.isWindows - ? getWindowsInfo() - : exports.isMacOS - ? getMacOsInfo() - : getLinuxInfo()))), { platform: exports.platform, - arch: exports.arch, - isWindows: exports.isWindows, - isMacOS: exports.isMacOS, - isLinux: exports.isLinux }); - }); -} -//# sourceMappingURL=platform.js.map - -/***/ }), - -/***/ 91785: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { - -"use strict"; - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; -const os_1 = __nccwpck_require__(22037); -const fs_1 = __nccwpck_require__(57147); -const { access, appendFile, writeFile } = fs_1.promises; -exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; -exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'; -class Summary { - constructor() { - this._buffer = ''; - } - /** - * Finds the summary file path from the environment, rejects if env var is not found or file does not exist - * Also checks r/w permissions. - * - * @returns step summary file path - */ - filePath() { - return __awaiter(this, void 0, void 0, function* () { - if (this._filePath) { - return this._filePath; - } - const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR]; - if (!pathFromEnv) { - throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); - } - try { - yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK); - } - catch (_a) { - throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); - } - this._filePath = pathFromEnv; - return this._filePath; - }); - } - /** - * Wraps content in an HTML tag, adding any HTML attributes - * - * @param {string} tag HTML tag to wrap - * @param {string | null} content content within the tag - * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add - * - * @returns {string} content wrapped in HTML element - */ - wrap(tag, content, attrs = {}) { - const htmlAttrs = Object.entries(attrs) - .map(([key, value]) => ` ${key}="${value}"`) - .join(''); - if (!content) { - return `<${tag}${htmlAttrs}>`; - } - return `<${tag}${htmlAttrs}>${content}`; - } - /** - * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. - * - * @param {SummaryWriteOptions} [options] (optional) options for write operation - * - * @returns {Promise} summary instance - */ - write(options) { - return __awaiter(this, void 0, void 0, function* () { - const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); - const filePath = yield this.filePath(); - const writeFunc = overwrite ? writeFile : appendFile; - yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); - return this.emptyBuffer(); - }); - } - /** - * Clears the summary buffer and wipes the summary file - * - * @returns {Summary} summary instance - */ - clear() { - return __awaiter(this, void 0, void 0, function* () { - return this.emptyBuffer().write({ overwrite: true }); - }); - } - /** - * Returns the current summary buffer as a string - * - * @returns {string} string of summary buffer - */ - stringify() { - return this._buffer; - } - /** - * If the summary buffer is empty - * - * @returns {boolen} true if the buffer is empty - */ - isEmptyBuffer() { - return this._buffer.length === 0; - } - /** - * Resets the summary buffer without writing to summary file - * - * @returns {Summary} summary instance - */ - emptyBuffer() { - this._buffer = ''; - return this; - } - /** - * Adds raw text to the summary buffer - * - * @param {string} text content to add - * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) - * - * @returns {Summary} summary instance - */ - addRaw(text, addEOL = false) { - this._buffer += text; - return addEOL ? this.addEOL() : this; - } - /** - * Adds the operating system-specific end-of-line marker to the buffer - * - * @returns {Summary} summary instance - */ - addEOL() { - return this.addRaw(os_1.EOL); - } - /** - * Adds an HTML codeblock to the summary buffer - * - * @param {string} code content to render within fenced code block - * @param {string} lang (optional) language to syntax highlight code - * - * @returns {Summary} summary instance - */ - addCodeBlock(code, lang) { - const attrs = Object.assign({}, (lang && { lang })); - const element = this.wrap('pre', this.wrap('code', code), attrs); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML list to the summary buffer - * - * @param {string[]} items list of items to render - * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) - * - * @returns {Summary} summary instance - */ - addList(items, ordered = false) { - const tag = ordered ? 'ol' : 'ul'; - const listItems = items.map(item => this.wrap('li', item)).join(''); - const element = this.wrap(tag, listItems); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML table to the summary buffer - * - * @param {SummaryTableCell[]} rows table rows - * - * @returns {Summary} summary instance - */ - addTable(rows) { - const tableBody = rows - .map(row => { - const cells = row - .map(cell => { - if (typeof cell === 'string') { - return this.wrap('td', cell); - } - const { header, data, colspan, rowspan } = cell; - const tag = header ? 'th' : 'td'; - const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); - return this.wrap(tag, data, attrs); - }) - .join(''); - return this.wrap('tr', cells); - }) - .join(''); - const element = this.wrap('table', tableBody); - return this.addRaw(element).addEOL(); - } - /** - * Adds a collapsable HTML details element to the summary buffer - * - * @param {string} label text for the closed state - * @param {string} content collapsable content - * - * @returns {Summary} summary instance - */ - addDetails(label, content) { - const element = this.wrap('details', this.wrap('summary', label) + content); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML image tag to the summary buffer - * - * @param {string} src path to the image you to embed - * @param {string} alt text description of the image - * @param {SummaryImageOptions} options (optional) addition image attributes - * - * @returns {Summary} summary instance - */ - addImage(src, alt, options) { - const { width, height } = options || {}; - const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); - const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML section heading element - * - * @param {string} text heading text - * @param {number | string} [level=1] (optional) the heading level, default: 1 - * - * @returns {Summary} summary instance - */ - addHeading(text, level) { - const tag = `h${level}`; - const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) - ? tag - : 'h1'; - const element = this.wrap(allowedTag, text); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML thematic break (
) to the summary buffer - * - * @returns {Summary} summary instance - */ - addSeparator() { - const element = this.wrap('hr', null); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML line break (
) to the summary buffer - * - * @returns {Summary} summary instance - */ - addBreak() { - const element = this.wrap('br', null); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML blockquote to the summary buffer - * - * @param {string} text quote text - * @param {string} cite (optional) citation url - * - * @returns {Summary} summary instance - */ - addQuote(text, cite) { - const attrs = Object.assign({}, (cite && { cite })); - const element = this.wrap('blockquote', text, attrs); - return this.addRaw(element).addEOL(); - } - /** - * Adds an HTML anchor tag to the summary buffer - * - * @param {string} text link text/content - * @param {string} href hyperlink - * - * @returns {Summary} summary instance - */ - addLink(text, href) { - const element = this.wrap('a', text, { href }); - return this.addRaw(element).addEOL(); - } -} -const _summary = new Summary(); -/** - * @deprecated use `core.summary` - */ -exports.markdownSummary = _summary; -exports.summary = _summary; -//# sourceMappingURL=summary.js.map - -/***/ }), - -/***/ 15206: -/***/ ((__unused_webpack_module, exports) => { - -"use strict"; - -// We use any as a valid input type -/* eslint-disable @typescript-eslint/no-explicit-any */ -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.toCommandValue = toCommandValue; -exports.toCommandProperties = toCommandProperties; -/** - * Sanitizes an input into a string so it can be passed into issueCommand safely - * @param input input to sanitize into a string - */ -function toCommandValue(input) { - if (input === null || input === undefined) { - return ''; - } - else if (typeof input === 'string' || input instanceof String) { - return input; - } - return JSON.stringify(input); -} -/** - * - * @param annotationProperties - * @returns The command properties to send with the actual annotation command - * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 - */ -function toCommandProperties(annotationProperties) { - if (!Object.keys(annotationProperties).length) { - return {}; - } - return { - title: annotationProperties.title, - file: annotationProperties.file, - line: annotationProperties.startLine, - endLine: annotationProperties.endLine, - col: annotationProperties.startColumn, - endColumn: annotationProperties.endColumn - }; -} -//# sourceMappingURL=utils.js.map - -/***/ }), - /***/ 18538: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { @@ -2248,94 +1040,6 @@ function getOctokitOptions(token, options) { /***/ }), -/***/ 57281: -/***/ (function(__unused_webpack_module, exports) { - -"use strict"; - -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.PersonalAccessTokenCredentialHandler = exports.BearerCredentialHandler = exports.BasicCredentialHandler = void 0; -class BasicCredentialHandler { - constructor(username, password) { - this.username = username; - this.password = password; - } - prepareRequest(options) { - if (!options.headers) { - throw Error('The request has no headers'); - } - options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`; - } - // This handler cannot handle 401 - canHandleAuthentication() { - return false; - } - handleAuthentication() { - return __awaiter(this, void 0, void 0, function* () { - throw new Error('not implemented'); - }); - } -} -exports.BasicCredentialHandler = BasicCredentialHandler; -class BearerCredentialHandler { - constructor(token) { - this.token = token; - } - // currently implements pre-authorization - // TODO: support preAuth = false where it hooks on 401 - prepareRequest(options) { - if (!options.headers) { - throw Error('The request has no headers'); - } - options.headers['Authorization'] = `Bearer ${this.token}`; - } - // This handler cannot handle 401 - canHandleAuthentication() { - return false; - } - handleAuthentication() { - return __awaiter(this, void 0, void 0, function* () { - throw new Error('not implemented'); - }); - } -} -exports.BearerCredentialHandler = BearerCredentialHandler; -class PersonalAccessTokenCredentialHandler { - constructor(token) { - this.token = token; - } - // currently implements pre-authorization - // TODO: support preAuth = false where it hooks on 401 - prepareRequest(options) { - if (!options.headers) { - throw Error('The request has no headers'); - } - options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`; - } - // This handler cannot handle 401 - canHandleAuthentication() { - return false; - } - handleAuthentication() { - return __awaiter(this, void 0, void 0, function* () { - throw new Error('not implemented'); - }); - } -} -exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; -//# sourceMappingURL=auth.js.map - -/***/ }), - /***/ 75784: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { @@ -39904,7 +38608,9 @@ function renderLocalActionResults(results, catalog = publication_message_catalog } const errorsContent = results .filter(result => result.errors.length > 0) - .map(result => chalk_1.default.gray(result.errors.map(application_error_presentation_policy_1.renderApplicationErrorText).join('\n\n'))).join('\n'); + .map(result => chalk_1.default.gray(result.errors + .map(error => (0, application_error_presentation_policy_1.renderApplicationErrorText)(error, catalog.render)) + .join('\n\n'))).join('\n'); if (errorsContent.length > 0) { content += '\n' + chalk_1.default.red(`${catalog.cli.errors}:`) + '\n' + errorsContent; } @@ -39976,39 +38682,6 @@ async function dispatchMainRunRoute(route, execution, handlers) { "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -40020,16 +38693,15 @@ exports.logWelcomeMessage = logWelcomeMessage; exports.runTokenExecution = runTokenExecution; exports.runNoIssueExecution = runNoIssueExecution; exports.runMainRoute = runMainRoute; -const core = __importStar(__nccwpck_require__(75855)); const chalk_1 = __importDefault(__nccwpck_require__(8578)); const boxen_1 = __importDefault(__nccwpck_require__(11652)); +const result_1 = __nccwpck_require__(73817); const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const main_run_dispatcher_1 = __nccwpck_require__(28586); const workflow_context_1 = __nccwpck_require__(55224); const workflow_queue_composition_root_1 = __nccwpck_require__(21598); const application_error_1 = __nccwpck_require__(75999); -const application_error_presentation_policy_1 = __nccwpck_require__(95067); exports.WORKFLOW_QUEUE_FAILURE_MESSAGE = 'Workflow queue check failed; sequential execution was not bypassed.'; /** * Keeps provider diagnostics out of the action's externally visible failure @@ -40104,9 +38776,14 @@ async function runMainRoute(execution, route, routeHandlers) { try { let results; if (route === 'unhandled') { - (0, logger_1.logError)(`Action not handled. Event: ${execution.eventName}.`); - core.setFailed('Action not handled.'); - results = []; + const semanticError = (0, application_error_1.toApplicationError)(undefined, 'workflow.invalid-event', 'Action not handled.'); + (0, logger_1.logError)(semanticError); + results = [new result_1.Result({ + id: 'MainRunRoute', + success: false, + executed: false, + errors: [semanticError], + })]; } else { results = await (0, main_run_dispatcher_1.dispatchMainRunRoute)(route, execution, routeHandlers); @@ -40118,8 +38795,12 @@ async function runMainRoute(execution, route, routeHandlers) { catch (error) { const semanticError = (0, application_error_1.toApplicationError)(error, 'workflow.failed', 'Main run failed.'); (0, logger_1.logError)(semanticError); - core.setFailed((0, application_error_presentation_policy_1.renderApplicationErrorText)(semanticError)); - return []; + return [new result_1.Result({ + id: 'MainRunRoute', + success: false, + executed: true, + errors: [semanticError], + })]; } } @@ -40623,12 +39304,13 @@ exports.TITLE = 'Copilot'; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = void 0; +exports.ApplicationError = exports.APPLICATION_ERROR_RECOVERY_IDS = exports.APPLICATION_ERROR_METADATA = void 0; exports.toApplicationError = toApplicationError; const application_error_1 = __nccwpck_require__(97790); const application_error_context_1 = __nccwpck_require__(4034); var application_error_2 = __nccwpck_require__(97790); Object.defineProperty(exports, "APPLICATION_ERROR_METADATA", ({ enumerable: true, get: function () { return application_error_2.APPLICATION_ERROR_METADATA; } })); +Object.defineProperty(exports, "APPLICATION_ERROR_RECOVERY_IDS", ({ enumerable: true, get: function () { return application_error_2.APPLICATION_ERROR_RECOVERY_IDS; } })); /** Creates a semantic error and owns correlation identity outside the pure model. */ class ApplicationError extends application_error_1.ApplicationError { constructor(code, message, options = {}) { @@ -41532,35 +40214,300 @@ function resolveThinkAgentTask(commandName, destinationType) { } +/***/ }), + +/***/ 64809: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readEnglishApplicationErrorMessage = exports.APPLICATION_ERROR_CATALOG_DEFINITIONS = exports.SPANISH_APPLICATION_ERROR_DEFINITION = exports.ENGLISH_APPLICATION_ERROR_DEFINITION = exports.SPANISH_APPLICATION_ERROR_MESSAGES = exports.ENGLISH_APPLICATION_ERROR_MESSAGES = exports.APPLICATION_ERROR_MESSAGE_IDS = exports.APPLICATION_ERROR_CODES = void 0; +exports.resolveStaticApplicationErrorCatalog = resolveStaticApplicationErrorCatalog; +exports.resolveApplicationErrorCatalog = resolveApplicationErrorCatalog; +const application_error_1 = __nccwpck_require__(97790); +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +const LABEL_KEYS = Object.freeze([ + 'impact', 'errorCode', 'action', 'retainedState', 'retryable', 'yes', 'no', 'reference', +]); +const CONTENT_FIELDS = Object.freeze(['impact', 'action', 'retainedState']); +exports.APPLICATION_ERROR_CODES = Object.freeze(Object.keys(application_error_1.APPLICATION_ERROR_METADATA)); +exports.APPLICATION_ERROR_MESSAGE_IDS = Object.freeze([ + ...LABEL_KEYS.map(key => `error.label.${key}`), + ...exports.APPLICATION_ERROR_CODES.flatMap(code => CONTENT_FIELDS.map(field => `error.${code}.${field}`)), + ...application_error_1.APPLICATION_ERROR_RECOVERY_IDS.flatMap(id => CONTENT_FIELDS + .map(field => `error.recovery.${id}.${field}`)), +]); +const ENGLISH_LABELS = Object.freeze({ + impact: 'Impact', + errorCode: 'Error code', + action: 'Action', + retainedState: 'Retained state', + retryable: 'Retryable', + yes: 'Yes', + no: 'No', + reference: 'Reference', +}); +const SPANISH_LABELS = Object.freeze({ + impact: 'Impacto', + errorCode: 'Código de error', + action: 'Acción', + retainedState: 'Estado conservado', + retryable: 'Reintentable', + yes: 'Sí', + no: 'No', + reference: 'Referencia', +}); +const ENGLISH_CONTENT = Object.freeze(Object.fromEntries(exports.APPLICATION_ERROR_CODES.map(code => [code, Object.freeze({ + impact: application_error_1.APPLICATION_ERROR_METADATA[code].impact, + action: application_error_1.APPLICATION_ERROR_METADATA[code].action, + retainedState: application_error_1.APPLICATION_ERROR_METADATA[code].retainedState, + })]))); +const UNCHANGED_STATE_ES = 'No se creó ningún estado ni efecto externo nuevo.'; +const PRESERVED_STATE_ES = 'Se conservaron el estado persistente existente y los efectos externos completados.'; +const SPANISH_CONTENT = Object.freeze({ + 'configuration.invalid': Object.freeze({ + impact: 'La operación no pudo usar los valores configurados.', + action: 'Corrige la configuración no válida y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'configuration.unsupported': Object.freeze({ + impact: 'La capacidad solicitada no es compatible con esta instalación.', + action: 'Usa una configuración compatible o actualiza la instalación.', + retainedState: UNCHANGED_STATE_ES, + }), + 'authorization.denied': Object.freeze({ + impact: 'La operación no pudo acceder al recurso necesario.', + action: 'Concede el permiso documentado y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'authorization.credential-invalid': Object.freeze({ + impact: 'La operación no pudo autenticarse con el proveedor necesario.', + action: 'Sustituye o configura la credencial necesaria y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'provider.not-found': Object.freeze({ + impact: 'No se encontró un recurso necesario del proveedor.', + action: 'Verifica el recurso de destino y reintenta la operación.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.conflict': Object.freeze({ + impact: 'El proveedor rechazó un estado actual conflictivo.', + action: 'Vuelve a cargar el estado actual y reinténtalo si la operación sigue siendo necesaria.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.rate-limited': Object.freeze({ + impact: 'El proveedor limitó temporalmente la operación.', + action: 'Reinténtalo cuando se restablezca el límite del proveedor.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.unavailable': Object.freeze({ + impact: 'El proveedor no estaba disponible temporalmente.', + action: 'Reinténtalo cuando el proveedor esté disponible.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.contract-invalid': Object.freeze({ + impact: 'La respuesta del proveedor no se pudo interpretar de forma segura.', + action: 'Revisa la integración del proveedor antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), + 'agent.policy-rejected': Object.freeze({ + impact: 'El agente configurado no se inició.', + action: 'Usa una configuración de agente permitida y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'agent.failed': Object.freeze({ + impact: 'El agente admitido no produjo un resultado utilizable.', + action: 'Revisa el estado saneado del agente y reinténtalo si procede.', + retainedState: PRESERVED_STATE_ES, + }), + 'locale.output-invalid': Object.freeze({ + impact: 'El contenido de producto generado por el agente se rechazó antes de publicarse porque su contrato de locale no era válido.', + action: 'Reinténtalo con un proveedor compatible con el locale configurado del repositorio.', + retainedState: UNCHANGED_STATE_ES, + }), + 'locale.translation-failed': Object.freeze({ + impact: 'La solicitud no se pudo interpretar de forma segura en el idioma configurado del repositorio.', + action: 'Reformula la solicitud o reinténtalo cuando esté disponible el proveedor de idioma configurado.', + retainedState: UNCHANGED_STATE_ES, + }), + 'validation.invalid-input': Object.freeze({ + impact: 'La operación no aceptó la entrada suministrada.', + action: 'Corrige la entrada y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'workflow.invalid-event': Object.freeze({ + impact: 'El evento no puede iniciar el workflow solicitado.', + action: 'Inicia la operación desde un evento o superficie compatible.', + retainedState: UNCHANGED_STATE_ES, + }), + 'workflow.stale': Object.freeze({ + impact: 'Un estado más reciente sustituyó esta ejecución del workflow.', + action: 'Revisa el estado actual e inicia una nueva ejecución solo si es necesario.', + retainedState: PRESERVED_STATE_ES, + }), + 'workflow.cancelled': Object.freeze({ + impact: 'El workflow se detuvo antes de completarse.', + action: 'Inicia una nueva ejecución si la operación sigue siendo necesaria.', + retainedState: PRESERVED_STATE_ES, + }), + 'workflow.failed': Object.freeze({ + impact: 'El workflow no pudo completar la operación solicitada.', + action: 'Revisa el estado actual y reintenta el paso fallido.', + retainedState: PRESERVED_STATE_ES, + }), + timeout: Object.freeze({ + impact: 'La operación superó su tiempo de ejecución limitado.', + action: 'Verifica el estado actual antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), + unexpected: Object.freeze({ + impact: 'La operación se detuvo porque un fallo inesperado se gestionó de forma segura.', + action: 'Usa el ID de correlación para investigar antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), +}); +const ENGLISH_RECOVERY_CONTENT = Object.freeze({ + 'pull-request-link-restored': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Rerun the workflow; no temporary pull-request state needs manual recovery.', + retainedState: 'The original pull-request base and description were restored.', + }), + 'pull-request-link-base-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Restore the temporary default base branch, then rerun the workflow.', + retainedState: 'The temporary default base branch remains; the original description was restored.', + }), + 'pull-request-link-reference-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Remove the temporary issue reference from the description, then rerun the workflow.', + retainedState: 'The original base branch was restored; the temporary issue reference remains in the description.', + }), + 'pull-request-link-base-and-reference-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Restore the temporary default base branch and remove the temporary issue reference, then rerun the workflow.', + retainedState: 'The temporary default base branch and issue reference remain.', + }), + 'managed-branch-enrichment-failed': Object.freeze({ + impact: 'The linked branch exists, but later issue metadata may be incomplete.', + action: 'Continue on {branchName} and rerun issue enrichment.', + retainedState: 'Branch {branchName} and its configuration patch were preserved.', + }), + 'inactivity-explanation-failed': Object.freeze({ + impact: 'Issue #{issueNumber} was closed without its terminal inactivity explanation.', + action: 'Inspect issue #{issueNumber} and add the explanation manually if the missing context matters.', + retainedState: 'Issue #{issueNumber} remains closed; the completed close will not be repeated.', + }), +}); +const SPANISH_RECOVERY_CONTENT = Object.freeze({ + 'pull-request-link-restored': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Vuelve a ejecutar el workflow; ningún estado temporal de la pull request requiere recuperación manual.', + retainedState: 'Se restauraron la rama base y la descripción originales de la pull request.', + }), + 'pull-request-link-base-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Restaura la rama base predeterminada temporal y vuelve a ejecutar el workflow.', + retainedState: 'La rama base predeterminada temporal permanece; se restauró la descripción original.', + }), + 'pull-request-link-reference-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Elimina la referencia temporal a la issue de la descripción y vuelve a ejecutar el workflow.', + retainedState: 'Se restauró la rama base original; la referencia temporal a la issue permanece en la descripción.', + }), + 'pull-request-link-base-and-reference-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Restaura la rama base predeterminada temporal, elimina la referencia temporal a la issue y vuelve a ejecutar el workflow.', + retainedState: 'La rama base predeterminada temporal y la referencia a la issue permanecen.', + }), + 'managed-branch-enrichment-failed': Object.freeze({ + impact: 'La rama vinculada existe, pero los metadatos posteriores de la issue pueden estar incompletos.', + action: 'Continúa en {branchName} y vuelve a ejecutar el enriquecimiento de la issue.', + retainedState: 'Se conservaron la rama {branchName} y su parche de configuración.', + }), + 'inactivity-explanation-failed': Object.freeze({ + impact: 'La issue #{issueNumber} se cerró sin su explicación final sobre la inactividad.', + action: 'Revisa la issue #{issueNumber} y añade la explicación manualmente si falta contexto importante.', + retainedState: 'La issue #{issueNumber} permanece cerrada; el cierre completado no se repetirá.', + }), +}); +function catalogMessages(labels, content, recoveryContent) { + return Object.freeze({ + ...Object.fromEntries(LABEL_KEYS.map(key => [`error.label.${key}`, labels[key]])), + ...Object.fromEntries(exports.APPLICATION_ERROR_CODES.flatMap(code => CONTENT_FIELDS.map(field => [ + `error.${code}.${field}`, + content[code][field], + ]))), + ...Object.fromEntries(application_error_1.APPLICATION_ERROR_RECOVERY_IDS.flatMap(id => CONTENT_FIELDS.map(field => [ + `error.recovery.${id}.${field}`, + recoveryContent[id][field], + ]))), + }); +} +exports.ENGLISH_APPLICATION_ERROR_MESSAGES = catalogMessages(ENGLISH_LABELS, ENGLISH_CONTENT, ENGLISH_RECOVERY_CONTENT); +exports.SPANISH_APPLICATION_ERROR_MESSAGES = catalogMessages(SPANISH_LABELS, SPANISH_CONTENT, SPANISH_RECOVERY_CONTENT); +exports.ENGLISH_APPLICATION_ERROR_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'en-US', + compatibleBaseLanguage: 'en', + messages: exports.ENGLISH_APPLICATION_ERROR_MESSAGES, +}); +exports.SPANISH_APPLICATION_ERROR_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'es-ES', + compatibleBaseLanguage: 'es', + messages: exports.SPANISH_APPLICATION_ERROR_MESSAGES, +}); +exports.APPLICATION_ERROR_CATALOG_DEFINITIONS = Object.freeze([ + exports.ENGLISH_APPLICATION_ERROR_DEFINITION, + exports.SPANISH_APPLICATION_ERROR_DEFINITION, +]); +function resolveStaticApplicationErrorCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_APPLICATION_ERROR_DEFINITION, exports.APPLICATION_ERROR_CATALOG_DEFINITIONS); +} +function resolveApplicationErrorCatalog(locale, configuration, resolver) { + return (0, resolved_message_catalog_policy_1.resolveMessageCatalogView)(locale, exports.APPLICATION_ERROR_MESSAGE_IDS, exports.ENGLISH_APPLICATION_ERROR_DEFINITION, exports.APPLICATION_ERROR_CATALOG_DEFINITIONS, configuration, resolver); +} +const readEnglishApplicationErrorMessage = (id, variables = {}) => (0, message_catalog_1.renderCatalogMessage)(exports.ENGLISH_APPLICATION_ERROR_MESSAGES[id], variables, 'en-US'); +exports.readEnglishApplicationErrorMessage = readEnglishApplicationErrorMessage; + + /***/ }), /***/ 95067: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildApplicationErrorPresentation = buildApplicationErrorPresentation; exports.renderApplicationErrorText = renderApplicationErrorText; +const application_error_message_catalog_1 = __nccwpck_require__(64809); /** Shared semantic view model for terminal, GitHub, and API presentation. */ -function buildApplicationErrorPresentation(error) { +function buildApplicationErrorPresentation(error, message = application_error_message_catalog_1.readEnglishApplicationErrorMessage) { + const descriptor = error.recovery + ? `error.recovery.${error.recovery.id}` + : `error.${error.code}`; + const variables = error.recovery?.variables; return { - impact: error.impact, - cause: error.message, + impact: message(`${descriptor}.impact`, variables), code: error.code, - action: error.action, - retainedState: error.retainedState, + action: message(`${descriptor}.action`, variables), + retainedState: message(`${descriptor}.retainedState`, variables), + retryable: message(error.retryable ? 'error.label.yes' : 'error.label.no'), reference: error.correlationId, }; } -function renderApplicationErrorText(error) { - const view = buildApplicationErrorPresentation(error); +function renderApplicationErrorText(error, message = application_error_message_catalog_1.readEnglishApplicationErrorMessage) { + const view = buildApplicationErrorPresentation(error, message); return [ - `Impact: ${view.impact}`, - `Cause (${view.code}): ${view.cause}`, - `Action: ${view.action}`, - `Retained state: ${view.retainedState}`, - `Reference: ${view.reference}`, + `${message('error.label.impact')}: ${view.impact}`, + `${message('error.label.errorCode')}: ${view.code}`, + `${message('error.label.action')}: ${view.action}`, + `${message('error.label.retainedState')}: ${view.retainedState}`, + `${message('error.label.retryable')}: ${view.retryable}`, + `${message('error.label.reference')}: ${view.reference}`, ].join('\n'); } @@ -43565,6 +42512,7 @@ const deployment_operation_1 = __nccwpck_require__(92730); const message_catalog_1 = __nccwpck_require__(27097); const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); const merge_queue_message_catalog_1 = __nccwpck_require__(56033); +const application_error_message_catalog_1 = __nccwpck_require__(64809); const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'release', 'hotfix', 'currentStatus', 'noAction', 'actionRequired', 'progress', 'currentTransition', 'whatNext', 'links', 'technical', 'alreadyPublished', @@ -43617,6 +42565,7 @@ exports.DEPLOYMENT_MESSAGE_IDS = Object.freeze([ ...DIAGRAM_KEYS.map(key => `deployment.diagram.${key}`), ...TEMPLATE_MESSAGE_IDS, ...merge_queue_message_catalog_1.MERGE_QUEUE_MESSAGE_IDS, + ...application_error_message_catalog_1.APPLICATION_ERROR_MESSAGE_IDS, ]); const ENGLISH_SIMPLE = Object.freeze({ release: 'Release', hotfix: 'Hotfix', currentStatus: 'Current status', @@ -43750,26 +42699,27 @@ const SPANISH_TEMPLATES = Object.freeze({ 'deployment.milestone.reconciliationBlocked': '❌ Despliegue bloqueado: {reason}', 'deployment.milestone.complete': '✅ El despliegue {tag} y todos los destinos de reconciliación configurados se han completado.', }); -function catalogMessages(simple, phases, diagram, templates, mergeQueue) { +function catalogMessages(simple, phases, diagram, templates, mergeQueue, errorMessages) { return Object.freeze({ ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`deployment.${key}`, simple[key]])), ...Object.fromEntries(deployment_operation_1.DEPLOYMENT_PHASES.map(phase => [`deployment.phase.${phase}`, phases[phase]])), ...Object.fromEntries(DIAGRAM_KEYS.map(key => [`deployment.diagram.${key}`, diagram[key]])), ...templates, ...mergeQueue, + ...errorMessages, }); } exports.ENGLISH_DEPLOYMENT_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', - messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES, merge_queue_message_catalog_1.ENGLISH_MERGE_QUEUE_MESSAGES), + messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES, merge_queue_message_catalog_1.ENGLISH_MERGE_QUEUE_MESSAGES, application_error_message_catalog_1.ENGLISH_APPLICATION_ERROR_MESSAGES), }); exports.SPANISH_DEPLOYMENT_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', - messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES, merge_queue_message_catalog_1.SPANISH_MERGE_QUEUE_MESSAGES), + messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES, merge_queue_message_catalog_1.SPANISH_MERGE_QUEUE_MESSAGES, application_error_message_catalog_1.SPANISH_APPLICATION_ERROR_MESSAGES), }); exports.DEPLOYMENT_CATALOG_DEFINITIONS = Object.freeze([ exports.ENGLISH_DEPLOYMENT_DEFINITION, @@ -44390,9 +43340,6 @@ exports.INACTIVITY_MESSAGE_IDS = Object.freeze([ 'inactivity.error.revalidate', 'inactivity.error.close', 'inactivity.error.comment', - 'inactivity.error.commentImpact', - 'inactivity.error.commentAction', - 'inactivity.error.commentRetainedState', ]); const ENGLISH_MESSAGES = Object.freeze({ 'inactivity.closure.heading': 'Issue closed after inactivity', @@ -44418,9 +43365,6 @@ const ENGLISH_MESSAGES = Object.freeze({ 'inactivity.error.revalidate': 'Unable to recheck issue #{issueNumber} before inactivity closure.', 'inactivity.error.close': 'Unable to close issue #{issueNumber} after inactivity.', 'inactivity.error.comment': 'Issue #{issueNumber} was closed, but its inactivity explanation could not be published.', - 'inactivity.error.commentImpact': 'Issue #{issueNumber} was closed without its terminal inactivity explanation.', - 'inactivity.error.commentAction': 'Inspect issue #{issueNumber} and add the explanation manually if the missing context matters.', - 'inactivity.error.commentRetainedState': 'Issue #{issueNumber} remains closed; the completed close will not be repeated.', }); const SPANISH_MESSAGES = Object.freeze({ 'inactivity.closure.heading': 'Issue cerrada por inactividad', @@ -44450,9 +43394,6 @@ const SPANISH_MESSAGES = Object.freeze({ 'inactivity.error.revalidate': 'No se pudo volver a comprobar la issue #{issueNumber} antes de cerrarla por inactividad.', 'inactivity.error.close': 'No se pudo cerrar la issue #{issueNumber} por inactividad.', 'inactivity.error.comment': 'La issue #{issueNumber} se cerró, pero no se pudo publicar la explicación sobre su inactividad.', - 'inactivity.error.commentImpact': 'La issue #{issueNumber} se cerró sin su explicación final sobre la inactividad.', - 'inactivity.error.commentAction': 'Revisa la issue #{issueNumber} y añade la explicación manualmente si falta contexto importante.', - 'inactivity.error.commentRetainedState': 'La issue #{issueNumber} permanece cerrada; el cierre completado no se repetirá.', }); exports.ENGLISH_INACTIVITY_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, @@ -45021,7 +43962,8 @@ exports.resolvePublicationCatalog = resolvePublicationCatalog; exports.toPublicationCatalog = toPublicationCatalog; const message_catalog_1 = __nccwpck_require__(27097); const locale_1 = __nccwpck_require__(15386); -exports.PUBLICATION_MESSAGE_IDS = Object.freeze([ +const application_error_message_catalog_1 = __nccwpck_require__(64809); +const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', @@ -45092,6 +44034,10 @@ exports.PUBLICATION_MESSAGE_IDS = Object.freeze([ 'cli.errors', 'cli.reminder', ]); +exports.PUBLICATION_MESSAGE_IDS = Object.freeze([ + ...PUBLICATION_SURFACE_MESSAGE_IDS, + ...application_error_message_catalog_1.APPLICATION_ERROR_MESSAGE_IDS, +]); const ENGLISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Implementation plan', 'publication.planReady': 'Ready to start. No action is required from maintainers before implementation.', @@ -45162,6 +44108,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'cli.steps': 'Steps', 'cli.errors': 'Errors', 'cli.reminder': 'Reminder', + ...application_error_message_catalog_1.ENGLISH_APPLICATION_ERROR_MESSAGES, }); const SPANISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Plan de implementación', @@ -45233,6 +44180,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'cli.steps': 'Pasos', 'cli.errors': 'Errores', 'cli.reminder': 'Recordatorio', + ...application_error_message_catalog_1.SPANISH_APPLICATION_ERROR_MESSAGES, }); exports.ENGLISH_PUBLICATION_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, @@ -48538,13 +47486,10 @@ async function runCloseInactiveIssuesWorkflow(param, dependencies) { errors.push(new application_error_1.ApplicationError('provider.unavailable', message, { cause: error, retryable: false, - impact: resultMessages.message('inactivity.error.commentImpact', { - issueNumber: candidate.number, - }), - action: resultMessages.message('inactivity.error.commentAction', { - issueNumber: candidate.number, - }), - retainedState: resultMessages.message('inactivity.error.commentRetainedState', { issueNumber: candidate.number }), + recovery: { + id: 'inactivity-explanation-failed', + variables: { issueNumber: candidate.number }, + }, })); } } @@ -60733,9 +59678,10 @@ async function prepareManagedBranch(param, issueTitle, branches, taskId, depende } catch (error) { const semanticError = (0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'The branch was created, but its linked issue state could not be synchronized.', { - impact: 'The linked branch exists, but later issue metadata may be incomplete.', - action: 'Continue on the retained branch and rerun issue enrichment.', - retainedState: `The branch ${branchPayload.newBranchName} and its configuration patch were preserved.`, + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: branchPayload.newBranchName }, + }, }); result.push(new result_1.Result({ id: taskId, @@ -61248,8 +60194,7 @@ class LinkPullRequestIssueUseCase { catch (error) { const semanticError = (0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to link the pull request to its issue.', error instanceof link_pull_request_issue_workflow_1.PullRequestIssueLinkOperationError ? { - action: 'Restore any named temporary PR state, then rerun the workflow.', - retainedState: describeRetainedState(error), + recovery: pullRequestLinkRecovery(error), } : {}); (0, logging_ports_1.logError)(semanticError); @@ -61267,14 +60212,15 @@ class LinkPullRequestIssueUseCase { } } exports.LinkPullRequestIssueUseCase = LinkPullRequestIssueUseCase; -function describeRetainedState(error) { - if (!error.retainedBaseBranch && !error.retainedIssueReference) { - return 'The original pull-request base and description were restored.'; - } - return [ - error.retainedBaseBranch ? 'The temporary default base branch remains.' : 'The original base branch was restored.', - error.retainedIssueReference ? 'The temporary issue reference remains in the description.' : 'The original description was restored.', - ].join(' '); +function pullRequestLinkRecovery(error) { + const id = error.retainedBaseBranch + ? error.retainedIssueReference + ? 'pull-request-link-base-and-reference-retained' + : 'pull-request-link-base-retained' + : error.retainedIssueReference + ? 'pull-request-link-reference-retained' + : 'pull-request-link-restored'; + return { id, variables: {} }; } function describeRecovery(error) { const retainedState = [ @@ -64328,8 +63274,16 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function ( }; var _ApplicationError_cause; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = void 0; +exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = exports.APPLICATION_ERROR_RECOVERY_IDS = void 0; exports.isApplicationErrorCorrelationId = isApplicationErrorCorrelationId; +exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ + 'pull-request-link-restored', + 'pull-request-link-base-retained', + 'pull-request-link-reference-retained', + 'pull-request-link-base-and-reference-retained', + 'managed-branch-enrichment-failed', + 'inactivity-explanation-failed', +]); const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; exports.APPLICATION_ERROR_METADATA = { @@ -64477,10 +63431,11 @@ class ApplicationError extends Error { this.code = code; this.kind = metadata.kind; this.retryable = options.retryable ?? metadata.retryable; - this.impact = options.impact ?? metadata.impact; - this.action = options.action ?? metadata.action; - this.retainedState = options.retainedState ?? metadata.retainedState; + this.impact = metadata.impact; + this.action = metadata.action; + this.retainedState = metadata.retainedState; this.correlationId = correlationId; + this.recovery = normalizeApplicationErrorRecovery(options.recovery); __classPrivateFieldSet(this, _ApplicationError_cause, options.cause, "f"); } toJSON() { @@ -64494,11 +63449,49 @@ class ApplicationError extends Error { action: this.action, retainedState: this.retainedState, correlationId: this.correlationId, + ...(this.recovery ? { recovery: this.recovery } : {}), }; } } exports.ApplicationError = ApplicationError; _ApplicationError_cause = new WeakMap(); +const RECOVERY_VARIABLE_KEYS = Object.freeze({ + 'pull-request-link-restored': Object.freeze([]), + 'pull-request-link-base-retained': Object.freeze([]), + 'pull-request-link-reference-retained': Object.freeze([]), + 'pull-request-link-base-and-reference-retained': Object.freeze([]), + 'managed-branch-enrichment-failed': Object.freeze(['branchName']), + 'inactivity-explanation-failed': Object.freeze(['issueNumber']), +}); +function normalizeApplicationErrorRecovery(recovery) { + if (!recovery) + return undefined; + if (!exports.APPLICATION_ERROR_RECOVERY_IDS.includes(recovery.id)) { + throw new TypeError('Application error recovery ID is invalid.'); + } + const variables = recovery.variables; + const actualKeys = Object.keys(variables).sort(); + const expectedKeys = [...RECOVERY_VARIABLE_KEYS[recovery.id]].sort(); + if (actualKeys.length !== expectedKeys.length + || actualKeys.some((key, index) => key !== expectedKeys[index])) { + throw new TypeError(`Application error recovery variables are invalid for ${recovery.id}.`); + } + if (recovery.id === 'managed-branch-enrichment-failed' + && (typeof variables.branchName !== 'string' + || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/u.test(variables.branchName))) { + throw new TypeError('Application error recovery branch name is invalid.'); + } + if (recovery.id === 'inactivity-explanation-failed' + && (typeof variables.issueNumber !== 'number' + || !Number.isSafeInteger(variables.issueNumber) + || variables.issueNumber < 1)) { + throw new TypeError('Application error recovery issue number is invalid.'); + } + return Object.freeze({ + id: recovery.id, + variables: Object.freeze({ ...variables }), + }); +} /***/ }), @@ -74732,7 +73725,7 @@ exports.renderCatalogMessage = renderCatalogMessage; exports.catalogPlaceholders = catalogPlaceholders; exports.catalogPluralCategories = catalogPluralCategories; const locale_1 = __nccwpck_require__(15386); -exports.MESSAGE_CATALOG_VERSION = '1'; +exports.MESSAGE_CATALOG_VERSION = '2'; exports.CATALOG_PLURAL_CATEGORIES = Object.freeze([ 'zero', 'one', @@ -80274,6 +79267,7 @@ function logError(message, metadata) { action: message.action, retainedState: message.retainedState, correlationId: message.correlationId, + ...(message.recovery ? { recovery: message.recovery } : {}), }, }); emitLog({ level: 'error', message: sanitized, timestamp: Date.now(), metadata: safeMetadata }, console.error); diff --git a/build/github_action/index.js b/build/github_action/index.js index 7e2beb651..eb3d7b768 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -39377,6 +39377,7 @@ const agent_capability_composition_root_1 = __nccwpck_require__(85079); const resolve_message_catalog_use_case_1 = __nccwpck_require__(99961); const github_action_locale_inputs_1 = __nccwpck_require__(58893); const publication_message_catalog_1 = __nccwpck_require__(34223); +const application_error_message_catalog_1 = __nccwpck_require__(64809); async function runGitHubAction() { const eventInputs = (0, github_event_inputs_1.buildGithubActionEventInputs)({ payload: github.context.payload, @@ -39465,11 +39466,21 @@ async function runGitHubActionEntry(run = runGitHubAction) { catch (cause) { const semanticError = (0, application_error_1.toApplicationError)(cause, 'workflow.failed', 'GitHub Action execution failed.'); (0, logger_1.logError)(semanticError); - core.setFailed((0, application_error_presentation_policy_1.renderApplicationErrorText)(semanticError)); + core.setFailed((0, application_error_presentation_policy_1.renderApplicationErrorText)(semanticError, earlyGithubActionErrorMessage())); } }); } +function earlyGithubActionErrorMessage() { + try { + const locale = (0, github_action_locale_inputs_1.readGithubActionLocaleInputs)(github_action_input_1.getGithubActionInput).repository; + return (0, application_error_message_catalog_1.resolveStaticApplicationErrorCatalog)(locale).message; + } + catch { + return (0, application_error_message_catalog_1.resolveStaticApplicationErrorCatalog)('en-US').message; + } +} // Only auto-run when executed as the action entry (not when imported by tests) +/* istanbul ignore next -- the bundled production entry is covered by the Action smoke path. */ if (typeof process.env.JEST_WORKER_ID === 'undefined') { void runGitHubActionEntry(); } @@ -39647,9 +39658,11 @@ async function finishGithubAction(execution, results, issueNotificationPort, con } const summary = await writeActionSummary(execution, summaryPort, catalogResolver); if (!dryRun) - await publishCopilotEvidence(execution, results, summary, evidencePort); - failActionForUnresolvedFindingsIfConfigured(execution, results, dryRun); - setFirstErrorIfExists(results); + await publishCopilotEvidence(execution, results, summary.text, evidencePort); + const completionError = firstApplicationError(results) + ?? bugbotCompletionError(execution, results, dryRun); + if (completionError) + core.setFailed((0, application_error_presentation_policy_1.renderApplicationErrorText)(completionError, summary.errorMessage)); } function extractBugbotTelemetry(results) { return results.flatMap((result) => { @@ -39666,10 +39679,12 @@ async function writeActionSummary(execution, summaryPort, catalogResolver) { let body; let localizationLabels; let appendLocalizationEvidence = false; + let errorMessage; if (execution.singleAction.isDeploymentOrchestrationAction && operation) { const effectiveLocale = operation.locale ?? locale; const catalog = await (0, deployment_message_catalog_1.resolveDeploymentCatalog)(effectiveLocale.repository, execution.ai.getAgentConfiguration('planner'), catalogResolver); const messages = (0, deployment_message_catalog_1.deploymentCopy)(catalog); + errorMessage = (id, variables) => catalog.message(id, variables); localizationLabels = { heading: messages.localization, property: messages.property, @@ -39697,6 +39712,7 @@ async function writeActionSummary(execution, summaryPort, catalogResolver) { } else { const catalog = await (0, action_summary_message_catalog_1.resolveActionSummaryCatalog)(locale.repository, execution.ai.getAgentConfiguration('planner'), catalogResolver); + errorMessage = (id, variables) => catalog.message(id, variables); body = (0, action_summary_policy_1.buildActionSummary)({ owner: execution.owner, repository: execution.repo, @@ -39726,14 +39742,14 @@ async function writeActionSummary(execution, summaryPort, catalogResolver) { : ''; const summaryText = [body, localizationEvidence].filter(Boolean).join('\n\n'); if (!summaryPort) - return summaryText; + return { text: summaryText, errorMessage }; try { await summaryPort.publish(summaryText); } catch (error) { (0, logger_1.logInfo)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Could not write the GitHub Actions summary.').message); } - return summaryText; + return { text: summaryText, errorMessage }; } async function publishCopilotEvidence(execution, results, summary, evidencePort) { if (!evidencePort) @@ -39758,23 +39774,23 @@ async function publishCopilotEvidence(execution, results, summary, evidencePort) (0, logger_1.logInfo)((0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Could not publish the optional GitHub Check Run.').message); } } -function failActionForUnresolvedFindingsIfConfigured(execution, results, dryRun) { +function bugbotCompletionError(execution, results, dryRun) { if (dryRun) - return; + return undefined; const projection = (0, bugbot_result_finding_state_projection_policy_1.projectBugbotResultFindingStates)(results); if (projection.status === 'invalid') { - core.setFailed('Bugbot finding-state evidence is malformed.'); - return; + return new application_error_1.ApplicationError('provider.contract-invalid', 'Bugbot finding-state evidence is malformed.'); } if (projection.status === 'absent') - return; + return undefined; if (projection.counts.unknown > 0) { - core.setFailed(`Bugbot could not verify ${projection.counts.unknown} finding state(s).`); + return new application_error_1.ApplicationError('provider.contract-invalid', 'Bugbot finding-state evidence could not be verified.'); } - else if (execution.ai.getBugbotReviewConfiguration().failOnUnresolved + if (execution.ai.getBugbotReviewConfiguration().failOnUnresolved && (0, review_state_1.countActionableBugbotFindings)(projection.counts) > 0) { - core.setFailed(`Bugbot found ${(0, review_state_1.countActionableBugbotFindings)(projection.counts)} unresolved actionable finding(s).`); + return new application_error_1.ApplicationError('workflow.failed', 'Bugbot found unresolved actionable findings.'); } + return undefined; } function commitPublishedRecommendationState(execution, results) { const pendingState = results @@ -39787,13 +39803,13 @@ function commitPublishedRecommendationState(execution, results) { execution.currentConfiguration.recommendationState = pendingState; } } -function setFirstErrorIfExists(results) { +function firstApplicationError(results) { for (const result of results) { if (result.errors && result.errors.length > 0) { - core.setFailed((0, application_error_presentation_policy_1.renderApplicationErrorText)(result.errors[0])); - return; + return result.errors[0]; } } + return undefined; } @@ -40416,39 +40432,6 @@ async function dispatchMainRunRoute(route, execution, handlers) { "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -40460,16 +40443,15 @@ exports.logWelcomeMessage = logWelcomeMessage; exports.runTokenExecution = runTokenExecution; exports.runNoIssueExecution = runNoIssueExecution; exports.runMainRoute = runMainRoute; -const core = __importStar(__nccwpck_require__(75855)); const chalk_1 = __importDefault(__nccwpck_require__(8578)); const boxen_1 = __importDefault(__nccwpck_require__(11652)); +const result_1 = __nccwpck_require__(73817); const product_identity_1 = __nccwpck_require__(18739); const logger_1 = __nccwpck_require__(91151); const main_run_dispatcher_1 = __nccwpck_require__(28586); const workflow_context_1 = __nccwpck_require__(55224); const workflow_queue_composition_root_1 = __nccwpck_require__(21598); const application_error_1 = __nccwpck_require__(75999); -const application_error_presentation_policy_1 = __nccwpck_require__(95067); exports.WORKFLOW_QUEUE_FAILURE_MESSAGE = 'Workflow queue check failed; sequential execution was not bypassed.'; /** * Keeps provider diagnostics out of the action's externally visible failure @@ -40544,9 +40526,14 @@ async function runMainRoute(execution, route, routeHandlers) { try { let results; if (route === 'unhandled') { - (0, logger_1.logError)(`Action not handled. Event: ${execution.eventName}.`); - core.setFailed('Action not handled.'); - results = []; + const semanticError = (0, application_error_1.toApplicationError)(undefined, 'workflow.invalid-event', 'Action not handled.'); + (0, logger_1.logError)(semanticError); + results = [new result_1.Result({ + id: 'MainRunRoute', + success: false, + executed: false, + errors: [semanticError], + })]; } else { results = await (0, main_run_dispatcher_1.dispatchMainRunRoute)(route, execution, routeHandlers); @@ -40558,8 +40545,12 @@ async function runMainRoute(execution, route, routeHandlers) { catch (error) { const semanticError = (0, application_error_1.toApplicationError)(error, 'workflow.failed', 'Main run failed.'); (0, logger_1.logError)(semanticError); - core.setFailed((0, application_error_presentation_policy_1.renderApplicationErrorText)(semanticError)); - return []; + return [new result_1.Result({ + id: 'MainRunRoute', + success: false, + executed: true, + errors: [semanticError], + })]; } } @@ -41063,12 +41054,13 @@ exports.TITLE = 'Copilot'; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = void 0; +exports.ApplicationError = exports.APPLICATION_ERROR_RECOVERY_IDS = exports.APPLICATION_ERROR_METADATA = void 0; exports.toApplicationError = toApplicationError; const application_error_1 = __nccwpck_require__(97790); const application_error_context_1 = __nccwpck_require__(4034); var application_error_2 = __nccwpck_require__(97790); Object.defineProperty(exports, "APPLICATION_ERROR_METADATA", ({ enumerable: true, get: function () { return application_error_2.APPLICATION_ERROR_METADATA; } })); +Object.defineProperty(exports, "APPLICATION_ERROR_RECOVERY_IDS", ({ enumerable: true, get: function () { return application_error_2.APPLICATION_ERROR_RECOVERY_IDS; } })); /** Creates a semantic error and owns correlation identity outside the pure model. */ class ApplicationError extends application_error_1.ApplicationError { constructor(code, message, options = {}) { @@ -41137,15 +41129,14 @@ exports.resolveStaticActionSummaryCatalog = resolveStaticActionSummaryCatalog; exports.resolveActionSummaryCatalog = resolveActionSummaryCatalog; const message_catalog_1 = __nccwpck_require__(27097); const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +const application_error_message_catalog_1 = __nccwpck_require__(64809); const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'heading', 'repository', 'property', 'value', 'status', 'event', 'target', 'lifecycle', 'descriptionPolicy', 'results', 'findingStates', 'bugbotReview', 'resultDetails', 'localization', 'repositoryLocale', 'issueLocale', 'pullRequestLocale', 'catalogResolution', 'descriptors', 'reason', 'failure', 'findings', 'partial', 'superseded', 'skipped', 'dryRun', 'success', 'invalid', - 'none', 'impact', 'cause', 'action', - 'retainedState', 'reference', 'retryable', 'yes', 'no', - 'resultSucceeded', 'resultFailed', 'resultSkipped', + 'none', 'resultSucceeded', 'resultFailed', 'resultSkipped', ]); const TEMPLATE_MESSAGE_IDS = Object.freeze([ 'summary.target.pullRequest', @@ -41157,15 +41148,11 @@ const FINDING_STATE_KEYS = Object.freeze([ 'open', 'reopened', 'fixed', 'obsolete', 'dismissed', 'verification-required', 'unknown', ]); -const ERROR_KIND_KEYS = Object.freeze([ - 'configuration', 'authorization', 'provider', 'agent', 'validation', 'workflow', 'unknown', -]); -const ERROR_FIELD_KEYS = Object.freeze(['impact', 'action', 'retainedState']); exports.ACTION_SUMMARY_MESSAGE_IDS = Object.freeze([ ...SIMPLE_MESSAGE_KEYS.map(key => `summary.${key}`), ...TEMPLATE_MESSAGE_IDS, ...FINDING_STATE_KEYS.map(key => `summary.findingState.${key}`), - ...ERROR_KIND_KEYS.flatMap(kind => ERROR_FIELD_KEYS.map(field => `summary.error.${kind}.${field}`)), + ...application_error_message_catalog_1.APPLICATION_ERROR_MESSAGE_IDS, ]); const ENGLISH_SIMPLE = Object.freeze({ heading: 'Copilot execution', @@ -41197,14 +41184,6 @@ const ENGLISH_SIMPLE = Object.freeze({ success: 'Success', invalid: 'invalid', none: 'none', - impact: 'Impact', - cause: 'Error code', - action: 'Action', - retainedState: 'Retained state', - reference: 'Reference', - retryable: 'Retryable', - yes: 'Yes', - no: 'No', resultSucceeded: 'Succeeded', resultFailed: 'Failed', resultSkipped: 'Skipped', @@ -41239,14 +41218,6 @@ const SPANISH_SIMPLE = Object.freeze({ success: 'Correcto', invalid: 'no válido', none: 'ninguno', - impact: 'Impacto', - cause: 'Código de error', - action: 'Acción', - retainedState: 'Estado conservado', - reference: 'Referencia', - retryable: 'Reintentable', - yes: 'Sí', - no: 'No', resultSucceeded: 'Completado', resultFailed: 'Fallido', resultSkipped: 'Omitido', @@ -41281,102 +41252,25 @@ const SPANISH_FINDING_STATES = Object.freeze({ 'verification-required': 'requieren verificación', unknown: 'desconocidos', }); -const ENGLISH_ERRORS = Object.freeze({ - configuration: Object.freeze({ - impact: 'The operation could not use the configured values.', - action: 'Correct the configuration or choose a supported capability before retrying.', - retainedState: 'No new state or external effect was created.', - }), - authorization: Object.freeze({ - impact: 'The operation could not authenticate or access a required resource.', - action: 'Correct the credential or grant the documented permission before retrying.', - retainedState: 'No new state or external effect was created.', - }), - provider: Object.freeze({ - impact: 'A provider operation did not complete.', - action: 'Inspect the error code and retry only when the provider state or availability has changed.', - retainedState: 'Existing state and completed external effects remain in place.', - }), - agent: Object.freeze({ - impact: 'The configured agent did not produce usable product content.', - action: 'Inspect the sanitized agent status and retry with a compatible provider or model.', - retainedState: 'Existing state remains in place; rejected content was not published.', - }), - validation: Object.freeze({ - impact: 'The supplied input was rejected before the operation could continue.', - action: 'Correct the input and retry.', - retainedState: 'No new state or external effect was created.', - }), - workflow: Object.freeze({ - impact: 'The workflow could not complete the requested operation.', - action: 'Inspect the current state and retry only if the operation is still required.', - retainedState: 'Existing state and confirmed completed effects remain in place.', - }), - unknown: Object.freeze({ - impact: 'An unexpected failure was handled safely.', - action: 'Use the reference to investigate before retrying.', - retainedState: 'Existing state and confirmed completed effects remain in place.', - }), -}); -const SPANISH_ERRORS = Object.freeze({ - configuration: Object.freeze({ - impact: 'La operación no pudo usar los valores configurados.', - action: 'Corrige la configuración o elige una capacidad compatible antes de reintentarlo.', - retainedState: 'No se creó ningún estado ni efecto externo nuevo.', - }), - authorization: Object.freeze({ - impact: 'La operación no pudo autenticarse o acceder a un recurso necesario.', - action: 'Corrige la credencial o concede el permiso documentado antes de reintentarlo.', - retainedState: 'No se creó ningún estado ni efecto externo nuevo.', - }), - provider: Object.freeze({ - impact: 'Una operación del proveedor no se completó.', - action: 'Revisa el código de error y reinténtalo solo cuando haya cambiado el estado o la disponibilidad del proveedor.', - retainedState: 'El estado existente y los efectos externos completados se mantienen.', - }), - agent: Object.freeze({ - impact: 'El agente configurado no produjo contenido de producto utilizable.', - action: 'Revisa el estado saneado del agente y reinténtalo con un proveedor o modelo compatible.', - retainedState: 'El estado existente se mantiene y el contenido rechazado no se publicó.', - }), - validation: Object.freeze({ - impact: 'La entrada suministrada se rechazó antes de continuar la operación.', - action: 'Corrige la entrada y reinténtalo.', - retainedState: 'No se creó ningún estado ni efecto externo nuevo.', - }), - workflow: Object.freeze({ - impact: 'El workflow no pudo completar la operación solicitada.', - action: 'Revisa el estado actual y reinténtalo solo si la operación sigue siendo necesaria.', - retainedState: 'El estado existente y los efectos completados y confirmados se mantienen.', - }), - unknown: Object.freeze({ - impact: 'Un fallo inesperado se gestionó de forma segura.', - action: 'Usa la referencia para investigar antes de reintentarlo.', - retainedState: 'El estado existente y los efectos completados y confirmados se mantienen.', - }), -}); -function catalogMessages(simple, templates, findingStates, errors) { +function catalogMessages(simple, templates, findingStates, errorMessages) { return Object.freeze({ ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`summary.${key}`, simple[key]])), ...templates, ...Object.fromEntries(FINDING_STATE_KEYS.map(key => [`summary.findingState.${key}`, findingStates[key]])), - ...Object.fromEntries(ERROR_KIND_KEYS.flatMap(kind => ERROR_FIELD_KEYS.map(field => [ - `summary.error.${kind}.${field}`, - errors[kind][field], - ]))), + ...errorMessages, }); } exports.ENGLISH_ACTION_SUMMARY_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', - messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_TEMPLATES, ENGLISH_FINDING_STATES, ENGLISH_ERRORS), + messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_TEMPLATES, ENGLISH_FINDING_STATES, application_error_message_catalog_1.ENGLISH_APPLICATION_ERROR_MESSAGES), }); exports.SPANISH_ACTION_SUMMARY_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', - messages: catalogMessages(SPANISH_SIMPLE, SPANISH_TEMPLATES, SPANISH_FINDING_STATES, SPANISH_ERRORS), + messages: catalogMessages(SPANISH_SIMPLE, SPANISH_TEMPLATES, SPANISH_FINDING_STATES, application_error_message_catalog_1.SPANISH_APPLICATION_ERROR_MESSAGES), }); exports.ACTION_SUMMARY_CATALOG_DEFINITIONS = Object.freeze([ exports.ENGLISH_ACTION_SUMMARY_DEFINITION, @@ -41406,6 +41300,7 @@ const bugbot_telemetry_projection_policy_1 = __nccwpck_require__(43244); const bugbot_result_finding_state_projection_policy_1 = __nccwpck_require__(98117); const review_state_1 = __nccwpck_require__(79200); const action_summary_message_catalog_1 = __nccwpck_require__(61544); +const application_error_presentation_policy_1 = __nccwpck_require__(95067); const ENGLISH_LOCALIZATION_SUMMARY_LABELS = Object.freeze({ heading: 'Localization', property: 'Property', @@ -41578,16 +41473,18 @@ function resultFailed(result) { return result.errors.length > 0 || (!result.success && result.executed); } function renderFailures(results, catalog) { + const message = (id, variables) => catalogText(catalog, id, variables); return results.map(result => { const errors = result.errors .flatMap((error) => { + const view = (0, application_error_presentation_policy_1.buildApplicationErrorPresentation)(error, message); return [ - ` - **${catalogText(catalog, 'summary.impact')}:** ${errorCatalogText(catalog, error.kind, 'impact')}`, - ` - **${catalogText(catalog, 'summary.cause')}:** \`${error.code}\``, - ` - **${catalogText(catalog, 'summary.action')}:** ${errorCatalogText(catalog, error.kind, 'action')}`, - ` - **${catalogText(catalog, 'summary.retainedState')}:** ${errorCatalogText(catalog, error.kind, 'retainedState')}`, - ` - **${catalogText(catalog, 'summary.retryable')}:** ${catalogText(catalog, error.retryable ? 'summary.yes' : 'summary.no')}`, - ` - **${catalogText(catalog, 'summary.reference')}:** \`${error.correlationId}\``, + ` - **${message('error.label.impact')}:** ${view.impact}`, + ` - **${message('error.label.errorCode')}:** \`${view.code}\``, + ` - **${message('error.label.action')}:** ${view.action}`, + ` - **${message('error.label.retainedState')}:** ${view.retainedState}`, + ` - **${message('error.label.retryable')}:** ${view.retryable}`, + ` - **${message('error.label.reference')}:** \`${view.reference}\``, ]; }); return [ @@ -41596,9 +41493,6 @@ function renderFailures(results, catalog) { ].join('\n'); }).join('\n'); } -function errorCatalogText(catalog, kind, field) { - return catalogText(catalog, `summary.error.${kind}.${field}`); -} function catalogText(catalog, id, variables = {}) { return escapeMarkdownText(catalog.message(id, variables)); } @@ -42589,35 +42483,300 @@ function resolveThinkAgentTask(commandName, destinationType) { } +/***/ }), + +/***/ 64809: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readEnglishApplicationErrorMessage = exports.APPLICATION_ERROR_CATALOG_DEFINITIONS = exports.SPANISH_APPLICATION_ERROR_DEFINITION = exports.ENGLISH_APPLICATION_ERROR_DEFINITION = exports.SPANISH_APPLICATION_ERROR_MESSAGES = exports.ENGLISH_APPLICATION_ERROR_MESSAGES = exports.APPLICATION_ERROR_MESSAGE_IDS = exports.APPLICATION_ERROR_CODES = void 0; +exports.resolveStaticApplicationErrorCatalog = resolveStaticApplicationErrorCatalog; +exports.resolveApplicationErrorCatalog = resolveApplicationErrorCatalog; +const application_error_1 = __nccwpck_require__(97790); +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +const LABEL_KEYS = Object.freeze([ + 'impact', 'errorCode', 'action', 'retainedState', 'retryable', 'yes', 'no', 'reference', +]); +const CONTENT_FIELDS = Object.freeze(['impact', 'action', 'retainedState']); +exports.APPLICATION_ERROR_CODES = Object.freeze(Object.keys(application_error_1.APPLICATION_ERROR_METADATA)); +exports.APPLICATION_ERROR_MESSAGE_IDS = Object.freeze([ + ...LABEL_KEYS.map(key => `error.label.${key}`), + ...exports.APPLICATION_ERROR_CODES.flatMap(code => CONTENT_FIELDS.map(field => `error.${code}.${field}`)), + ...application_error_1.APPLICATION_ERROR_RECOVERY_IDS.flatMap(id => CONTENT_FIELDS + .map(field => `error.recovery.${id}.${field}`)), +]); +const ENGLISH_LABELS = Object.freeze({ + impact: 'Impact', + errorCode: 'Error code', + action: 'Action', + retainedState: 'Retained state', + retryable: 'Retryable', + yes: 'Yes', + no: 'No', + reference: 'Reference', +}); +const SPANISH_LABELS = Object.freeze({ + impact: 'Impacto', + errorCode: 'Código de error', + action: 'Acción', + retainedState: 'Estado conservado', + retryable: 'Reintentable', + yes: 'Sí', + no: 'No', + reference: 'Referencia', +}); +const ENGLISH_CONTENT = Object.freeze(Object.fromEntries(exports.APPLICATION_ERROR_CODES.map(code => [code, Object.freeze({ + impact: application_error_1.APPLICATION_ERROR_METADATA[code].impact, + action: application_error_1.APPLICATION_ERROR_METADATA[code].action, + retainedState: application_error_1.APPLICATION_ERROR_METADATA[code].retainedState, + })]))); +const UNCHANGED_STATE_ES = 'No se creó ningún estado ni efecto externo nuevo.'; +const PRESERVED_STATE_ES = 'Se conservaron el estado persistente existente y los efectos externos completados.'; +const SPANISH_CONTENT = Object.freeze({ + 'configuration.invalid': Object.freeze({ + impact: 'La operación no pudo usar los valores configurados.', + action: 'Corrige la configuración no válida y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'configuration.unsupported': Object.freeze({ + impact: 'La capacidad solicitada no es compatible con esta instalación.', + action: 'Usa una configuración compatible o actualiza la instalación.', + retainedState: UNCHANGED_STATE_ES, + }), + 'authorization.denied': Object.freeze({ + impact: 'La operación no pudo acceder al recurso necesario.', + action: 'Concede el permiso documentado y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'authorization.credential-invalid': Object.freeze({ + impact: 'La operación no pudo autenticarse con el proveedor necesario.', + action: 'Sustituye o configura la credencial necesaria y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'provider.not-found': Object.freeze({ + impact: 'No se encontró un recurso necesario del proveedor.', + action: 'Verifica el recurso de destino y reintenta la operación.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.conflict': Object.freeze({ + impact: 'El proveedor rechazó un estado actual conflictivo.', + action: 'Vuelve a cargar el estado actual y reinténtalo si la operación sigue siendo necesaria.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.rate-limited': Object.freeze({ + impact: 'El proveedor limitó temporalmente la operación.', + action: 'Reinténtalo cuando se restablezca el límite del proveedor.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.unavailable': Object.freeze({ + impact: 'El proveedor no estaba disponible temporalmente.', + action: 'Reinténtalo cuando el proveedor esté disponible.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.contract-invalid': Object.freeze({ + impact: 'La respuesta del proveedor no se pudo interpretar de forma segura.', + action: 'Revisa la integración del proveedor antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), + 'agent.policy-rejected': Object.freeze({ + impact: 'El agente configurado no se inició.', + action: 'Usa una configuración de agente permitida y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'agent.failed': Object.freeze({ + impact: 'El agente admitido no produjo un resultado utilizable.', + action: 'Revisa el estado saneado del agente y reinténtalo si procede.', + retainedState: PRESERVED_STATE_ES, + }), + 'locale.output-invalid': Object.freeze({ + impact: 'El contenido de producto generado por el agente se rechazó antes de publicarse porque su contrato de locale no era válido.', + action: 'Reinténtalo con un proveedor compatible con el locale configurado del repositorio.', + retainedState: UNCHANGED_STATE_ES, + }), + 'locale.translation-failed': Object.freeze({ + impact: 'La solicitud no se pudo interpretar de forma segura en el idioma configurado del repositorio.', + action: 'Reformula la solicitud o reinténtalo cuando esté disponible el proveedor de idioma configurado.', + retainedState: UNCHANGED_STATE_ES, + }), + 'validation.invalid-input': Object.freeze({ + impact: 'La operación no aceptó la entrada suministrada.', + action: 'Corrige la entrada y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'workflow.invalid-event': Object.freeze({ + impact: 'El evento no puede iniciar el workflow solicitado.', + action: 'Inicia la operación desde un evento o superficie compatible.', + retainedState: UNCHANGED_STATE_ES, + }), + 'workflow.stale': Object.freeze({ + impact: 'Un estado más reciente sustituyó esta ejecución del workflow.', + action: 'Revisa el estado actual e inicia una nueva ejecución solo si es necesario.', + retainedState: PRESERVED_STATE_ES, + }), + 'workflow.cancelled': Object.freeze({ + impact: 'El workflow se detuvo antes de completarse.', + action: 'Inicia una nueva ejecución si la operación sigue siendo necesaria.', + retainedState: PRESERVED_STATE_ES, + }), + 'workflow.failed': Object.freeze({ + impact: 'El workflow no pudo completar la operación solicitada.', + action: 'Revisa el estado actual y reintenta el paso fallido.', + retainedState: PRESERVED_STATE_ES, + }), + timeout: Object.freeze({ + impact: 'La operación superó su tiempo de ejecución limitado.', + action: 'Verifica el estado actual antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), + unexpected: Object.freeze({ + impact: 'La operación se detuvo porque un fallo inesperado se gestionó de forma segura.', + action: 'Usa el ID de correlación para investigar antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), +}); +const ENGLISH_RECOVERY_CONTENT = Object.freeze({ + 'pull-request-link-restored': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Rerun the workflow; no temporary pull-request state needs manual recovery.', + retainedState: 'The original pull-request base and description were restored.', + }), + 'pull-request-link-base-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Restore the temporary default base branch, then rerun the workflow.', + retainedState: 'The temporary default base branch remains; the original description was restored.', + }), + 'pull-request-link-reference-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Remove the temporary issue reference from the description, then rerun the workflow.', + retainedState: 'The original base branch was restored; the temporary issue reference remains in the description.', + }), + 'pull-request-link-base-and-reference-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Restore the temporary default base branch and remove the temporary issue reference, then rerun the workflow.', + retainedState: 'The temporary default base branch and issue reference remain.', + }), + 'managed-branch-enrichment-failed': Object.freeze({ + impact: 'The linked branch exists, but later issue metadata may be incomplete.', + action: 'Continue on {branchName} and rerun issue enrichment.', + retainedState: 'Branch {branchName} and its configuration patch were preserved.', + }), + 'inactivity-explanation-failed': Object.freeze({ + impact: 'Issue #{issueNumber} was closed without its terminal inactivity explanation.', + action: 'Inspect issue #{issueNumber} and add the explanation manually if the missing context matters.', + retainedState: 'Issue #{issueNumber} remains closed; the completed close will not be repeated.', + }), +}); +const SPANISH_RECOVERY_CONTENT = Object.freeze({ + 'pull-request-link-restored': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Vuelve a ejecutar el workflow; ningún estado temporal de la pull request requiere recuperación manual.', + retainedState: 'Se restauraron la rama base y la descripción originales de la pull request.', + }), + 'pull-request-link-base-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Restaura la rama base predeterminada temporal y vuelve a ejecutar el workflow.', + retainedState: 'La rama base predeterminada temporal permanece; se restauró la descripción original.', + }), + 'pull-request-link-reference-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Elimina la referencia temporal a la issue de la descripción y vuelve a ejecutar el workflow.', + retainedState: 'Se restauró la rama base original; la referencia temporal a la issue permanece en la descripción.', + }), + 'pull-request-link-base-and-reference-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Restaura la rama base predeterminada temporal, elimina la referencia temporal a la issue y vuelve a ejecutar el workflow.', + retainedState: 'La rama base predeterminada temporal y la referencia a la issue permanecen.', + }), + 'managed-branch-enrichment-failed': Object.freeze({ + impact: 'La rama vinculada existe, pero los metadatos posteriores de la issue pueden estar incompletos.', + action: 'Continúa en {branchName} y vuelve a ejecutar el enriquecimiento de la issue.', + retainedState: 'Se conservaron la rama {branchName} y su parche de configuración.', + }), + 'inactivity-explanation-failed': Object.freeze({ + impact: 'La issue #{issueNumber} se cerró sin su explicación final sobre la inactividad.', + action: 'Revisa la issue #{issueNumber} y añade la explicación manualmente si falta contexto importante.', + retainedState: 'La issue #{issueNumber} permanece cerrada; el cierre completado no se repetirá.', + }), +}); +function catalogMessages(labels, content, recoveryContent) { + return Object.freeze({ + ...Object.fromEntries(LABEL_KEYS.map(key => [`error.label.${key}`, labels[key]])), + ...Object.fromEntries(exports.APPLICATION_ERROR_CODES.flatMap(code => CONTENT_FIELDS.map(field => [ + `error.${code}.${field}`, + content[code][field], + ]))), + ...Object.fromEntries(application_error_1.APPLICATION_ERROR_RECOVERY_IDS.flatMap(id => CONTENT_FIELDS.map(field => [ + `error.recovery.${id}.${field}`, + recoveryContent[id][field], + ]))), + }); +} +exports.ENGLISH_APPLICATION_ERROR_MESSAGES = catalogMessages(ENGLISH_LABELS, ENGLISH_CONTENT, ENGLISH_RECOVERY_CONTENT); +exports.SPANISH_APPLICATION_ERROR_MESSAGES = catalogMessages(SPANISH_LABELS, SPANISH_CONTENT, SPANISH_RECOVERY_CONTENT); +exports.ENGLISH_APPLICATION_ERROR_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'en-US', + compatibleBaseLanguage: 'en', + messages: exports.ENGLISH_APPLICATION_ERROR_MESSAGES, +}); +exports.SPANISH_APPLICATION_ERROR_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, + locale: 'es-ES', + compatibleBaseLanguage: 'es', + messages: exports.SPANISH_APPLICATION_ERROR_MESSAGES, +}); +exports.APPLICATION_ERROR_CATALOG_DEFINITIONS = Object.freeze([ + exports.ENGLISH_APPLICATION_ERROR_DEFINITION, + exports.SPANISH_APPLICATION_ERROR_DEFINITION, +]); +function resolveStaticApplicationErrorCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_APPLICATION_ERROR_DEFINITION, exports.APPLICATION_ERROR_CATALOG_DEFINITIONS); +} +function resolveApplicationErrorCatalog(locale, configuration, resolver) { + return (0, resolved_message_catalog_policy_1.resolveMessageCatalogView)(locale, exports.APPLICATION_ERROR_MESSAGE_IDS, exports.ENGLISH_APPLICATION_ERROR_DEFINITION, exports.APPLICATION_ERROR_CATALOG_DEFINITIONS, configuration, resolver); +} +const readEnglishApplicationErrorMessage = (id, variables = {}) => (0, message_catalog_1.renderCatalogMessage)(exports.ENGLISH_APPLICATION_ERROR_MESSAGES[id], variables, 'en-US'); +exports.readEnglishApplicationErrorMessage = readEnglishApplicationErrorMessage; + + /***/ }), /***/ 95067: -/***/ ((__unused_webpack_module, exports) => { +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.buildApplicationErrorPresentation = buildApplicationErrorPresentation; exports.renderApplicationErrorText = renderApplicationErrorText; +const application_error_message_catalog_1 = __nccwpck_require__(64809); /** Shared semantic view model for terminal, GitHub, and API presentation. */ -function buildApplicationErrorPresentation(error) { +function buildApplicationErrorPresentation(error, message = application_error_message_catalog_1.readEnglishApplicationErrorMessage) { + const descriptor = error.recovery + ? `error.recovery.${error.recovery.id}` + : `error.${error.code}`; + const variables = error.recovery?.variables; return { - impact: error.impact, - cause: error.message, + impact: message(`${descriptor}.impact`, variables), code: error.code, - action: error.action, - retainedState: error.retainedState, + action: message(`${descriptor}.action`, variables), + retainedState: message(`${descriptor}.retainedState`, variables), + retryable: message(error.retryable ? 'error.label.yes' : 'error.label.no'), reference: error.correlationId, }; } -function renderApplicationErrorText(error) { - const view = buildApplicationErrorPresentation(error); +function renderApplicationErrorText(error, message = application_error_message_catalog_1.readEnglishApplicationErrorMessage) { + const view = buildApplicationErrorPresentation(error, message); return [ - `Impact: ${view.impact}`, - `Cause (${view.code}): ${view.cause}`, - `Action: ${view.action}`, - `Retained state: ${view.retainedState}`, - `Reference: ${view.reference}`, + `${message('error.label.impact')}: ${view.impact}`, + `${message('error.label.errorCode')}: ${view.code}`, + `${message('error.label.action')}: ${view.action}`, + `${message('error.label.retainedState')}: ${view.retainedState}`, + `${message('error.label.retryable')}: ${view.retryable}`, + `${message('error.label.reference')}: ${view.reference}`, ].join('\n'); } @@ -44748,6 +44907,7 @@ const deployment_operation_1 = __nccwpck_require__(92730); const message_catalog_1 = __nccwpck_require__(27097); const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); const merge_queue_message_catalog_1 = __nccwpck_require__(56033); +const application_error_message_catalog_1 = __nccwpck_require__(64809); const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'release', 'hotfix', 'currentStatus', 'noAction', 'actionRequired', 'progress', 'currentTransition', 'whatNext', 'links', 'technical', 'alreadyPublished', @@ -44800,6 +44960,7 @@ exports.DEPLOYMENT_MESSAGE_IDS = Object.freeze([ ...DIAGRAM_KEYS.map(key => `deployment.diagram.${key}`), ...TEMPLATE_MESSAGE_IDS, ...merge_queue_message_catalog_1.MERGE_QUEUE_MESSAGE_IDS, + ...application_error_message_catalog_1.APPLICATION_ERROR_MESSAGE_IDS, ]); const ENGLISH_SIMPLE = Object.freeze({ release: 'Release', hotfix: 'Hotfix', currentStatus: 'Current status', @@ -44933,26 +45094,27 @@ const SPANISH_TEMPLATES = Object.freeze({ 'deployment.milestone.reconciliationBlocked': '❌ Despliegue bloqueado: {reason}', 'deployment.milestone.complete': '✅ El despliegue {tag} y todos los destinos de reconciliación configurados se han completado.', }); -function catalogMessages(simple, phases, diagram, templates, mergeQueue) { +function catalogMessages(simple, phases, diagram, templates, mergeQueue, errorMessages) { return Object.freeze({ ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`deployment.${key}`, simple[key]])), ...Object.fromEntries(deployment_operation_1.DEPLOYMENT_PHASES.map(phase => [`deployment.phase.${phase}`, phases[phase]])), ...Object.fromEntries(DIAGRAM_KEYS.map(key => [`deployment.diagram.${key}`, diagram[key]])), ...templates, ...mergeQueue, + ...errorMessages, }); } exports.ENGLISH_DEPLOYMENT_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', - messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES, merge_queue_message_catalog_1.ENGLISH_MERGE_QUEUE_MESSAGES), + messages: catalogMessages(ENGLISH_SIMPLE, ENGLISH_PHASES, ENGLISH_DIAGRAM, ENGLISH_TEMPLATES, merge_queue_message_catalog_1.ENGLISH_MERGE_QUEUE_MESSAGES, application_error_message_catalog_1.ENGLISH_APPLICATION_ERROR_MESSAGES), }); exports.SPANISH_DEPLOYMENT_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', - messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES, merge_queue_message_catalog_1.SPANISH_MERGE_QUEUE_MESSAGES), + messages: catalogMessages(SPANISH_SIMPLE, SPANISH_PHASES, SPANISH_DIAGRAM, SPANISH_TEMPLATES, merge_queue_message_catalog_1.SPANISH_MERGE_QUEUE_MESSAGES, application_error_message_catalog_1.SPANISH_APPLICATION_ERROR_MESSAGES), }); exports.DEPLOYMENT_CATALOG_DEFINITIONS = Object.freeze([ exports.ENGLISH_DEPLOYMENT_DEFINITION, @@ -45599,9 +45761,6 @@ exports.INACTIVITY_MESSAGE_IDS = Object.freeze([ 'inactivity.error.revalidate', 'inactivity.error.close', 'inactivity.error.comment', - 'inactivity.error.commentImpact', - 'inactivity.error.commentAction', - 'inactivity.error.commentRetainedState', ]); const ENGLISH_MESSAGES = Object.freeze({ 'inactivity.closure.heading': 'Issue closed after inactivity', @@ -45627,9 +45786,6 @@ const ENGLISH_MESSAGES = Object.freeze({ 'inactivity.error.revalidate': 'Unable to recheck issue #{issueNumber} before inactivity closure.', 'inactivity.error.close': 'Unable to close issue #{issueNumber} after inactivity.', 'inactivity.error.comment': 'Issue #{issueNumber} was closed, but its inactivity explanation could not be published.', - 'inactivity.error.commentImpact': 'Issue #{issueNumber} was closed without its terminal inactivity explanation.', - 'inactivity.error.commentAction': 'Inspect issue #{issueNumber} and add the explanation manually if the missing context matters.', - 'inactivity.error.commentRetainedState': 'Issue #{issueNumber} remains closed; the completed close will not be repeated.', }); const SPANISH_MESSAGES = Object.freeze({ 'inactivity.closure.heading': 'Issue cerrada por inactividad', @@ -45659,9 +45815,6 @@ const SPANISH_MESSAGES = Object.freeze({ 'inactivity.error.revalidate': 'No se pudo volver a comprobar la issue #{issueNumber} antes de cerrarla por inactividad.', 'inactivity.error.close': 'No se pudo cerrar la issue #{issueNumber} por inactividad.', 'inactivity.error.comment': 'La issue #{issueNumber} se cerró, pero no se pudo publicar la explicación sobre su inactividad.', - 'inactivity.error.commentImpact': 'La issue #{issueNumber} se cerró sin su explicación final sobre la inactividad.', - 'inactivity.error.commentAction': 'Revisa la issue #{issueNumber} y añade la explicación manualmente si falta contexto importante.', - 'inactivity.error.commentRetainedState': 'La issue #{issueNumber} permanece cerrada; el cierre completado no se repetirá.', }); exports.ENGLISH_INACTIVITY_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, @@ -46351,7 +46504,8 @@ exports.resolvePublicationCatalog = resolvePublicationCatalog; exports.toPublicationCatalog = toPublicationCatalog; const message_catalog_1 = __nccwpck_require__(27097); const locale_1 = __nccwpck_require__(15386); -exports.PUBLICATION_MESSAGE_IDS = Object.freeze([ +const application_error_message_catalog_1 = __nccwpck_require__(64809); +const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', @@ -46422,6 +46576,10 @@ exports.PUBLICATION_MESSAGE_IDS = Object.freeze([ 'cli.errors', 'cli.reminder', ]); +exports.PUBLICATION_MESSAGE_IDS = Object.freeze([ + ...PUBLICATION_SURFACE_MESSAGE_IDS, + ...application_error_message_catalog_1.APPLICATION_ERROR_MESSAGE_IDS, +]); const ENGLISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Implementation plan', 'publication.planReady': 'Ready to start. No action is required from maintainers before implementation.', @@ -46492,6 +46650,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'cli.steps': 'Steps', 'cli.errors': 'Errors', 'cli.reminder': 'Reminder', + ...application_error_message_catalog_1.ENGLISH_APPLICATION_ERROR_MESSAGES, }); const SPANISH_MESSAGES = Object.freeze({ 'publication.implementationPlan': 'Plan de implementación', @@ -46563,6 +46722,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'cli.steps': 'Pasos', 'cli.errors': 'Errores', 'cli.reminder': 'Recordatorio', + ...application_error_message_catalog_1.SPANISH_APPLICATION_ERROR_MESSAGES, }); exports.ENGLISH_PUBLICATION_DEFINITION = Object.freeze({ version: message_catalog_1.MESSAGE_CATALOG_VERSION, @@ -49036,13 +49196,10 @@ async function runCloseInactiveIssuesWorkflow(param, dependencies) { errors.push(new application_error_1.ApplicationError('provider.unavailable', message, { cause: error, retryable: false, - impact: resultMessages.message('inactivity.error.commentImpact', { - issueNumber: candidate.number, - }), - action: resultMessages.message('inactivity.error.commentAction', { - issueNumber: candidate.number, - }), - retainedState: resultMessages.message('inactivity.error.commentRetainedState', { issueNumber: candidate.number }), + recovery: { + id: 'inactivity-explanation-failed', + variables: { issueNumber: candidate.number }, + }, })); } } @@ -60934,9 +61091,10 @@ async function prepareManagedBranch(param, issueTitle, branches, taskId, depende } catch (error) { const semanticError = (0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'The branch was created, but its linked issue state could not be synchronized.', { - impact: 'The linked branch exists, but later issue metadata may be incomplete.', - action: 'Continue on the retained branch and rerun issue enrichment.', - retainedState: `The branch ${branchPayload.newBranchName} and its configuration patch were preserved.`, + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: branchPayload.newBranchName }, + }, }); result.push(new result_1.Result({ id: taskId, @@ -61449,8 +61607,7 @@ class LinkPullRequestIssueUseCase { catch (error) { const semanticError = (0, application_error_1.toApplicationError)(error, 'provider.unavailable', 'Unable to link the pull request to its issue.', error instanceof link_pull_request_issue_workflow_1.PullRequestIssueLinkOperationError ? { - action: 'Restore any named temporary PR state, then rerun the workflow.', - retainedState: describeRetainedState(error), + recovery: pullRequestLinkRecovery(error), } : {}); (0, logging_ports_1.logError)(semanticError); @@ -61468,14 +61625,15 @@ class LinkPullRequestIssueUseCase { } } exports.LinkPullRequestIssueUseCase = LinkPullRequestIssueUseCase; -function describeRetainedState(error) { - if (!error.retainedBaseBranch && !error.retainedIssueReference) { - return 'The original pull-request base and description were restored.'; - } - return [ - error.retainedBaseBranch ? 'The temporary default base branch remains.' : 'The original base branch was restored.', - error.retainedIssueReference ? 'The temporary issue reference remains in the description.' : 'The original description was restored.', - ].join(' '); +function pullRequestLinkRecovery(error) { + const id = error.retainedBaseBranch + ? error.retainedIssueReference + ? 'pull-request-link-base-and-reference-retained' + : 'pull-request-link-base-retained' + : error.retainedIssueReference + ? 'pull-request-link-reference-retained' + : 'pull-request-link-restored'; + return { id, variables: {} }; } function describeRecovery(error) { const retainedState = [ @@ -62257,8 +62415,16 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function ( }; var _ApplicationError_cause; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = void 0; +exports.ApplicationError = exports.APPLICATION_ERROR_METADATA = exports.APPLICATION_ERROR_RECOVERY_IDS = void 0; exports.isApplicationErrorCorrelationId = isApplicationErrorCorrelationId; +exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ + 'pull-request-link-restored', + 'pull-request-link-base-retained', + 'pull-request-link-reference-retained', + 'pull-request-link-base-and-reference-retained', + 'managed-branch-enrichment-failed', + 'inactivity-explanation-failed', +]); const PRESERVED_STATE = 'Existing persisted state and completed external effects were preserved.'; const UNCHANGED_STATE = 'No new state or external effect was created.'; exports.APPLICATION_ERROR_METADATA = { @@ -62406,10 +62572,11 @@ class ApplicationError extends Error { this.code = code; this.kind = metadata.kind; this.retryable = options.retryable ?? metadata.retryable; - this.impact = options.impact ?? metadata.impact; - this.action = options.action ?? metadata.action; - this.retainedState = options.retainedState ?? metadata.retainedState; + this.impact = metadata.impact; + this.action = metadata.action; + this.retainedState = metadata.retainedState; this.correlationId = correlationId; + this.recovery = normalizeApplicationErrorRecovery(options.recovery); __classPrivateFieldSet(this, _ApplicationError_cause, options.cause, "f"); } toJSON() { @@ -62423,11 +62590,49 @@ class ApplicationError extends Error { action: this.action, retainedState: this.retainedState, correlationId: this.correlationId, + ...(this.recovery ? { recovery: this.recovery } : {}), }; } } exports.ApplicationError = ApplicationError; _ApplicationError_cause = new WeakMap(); +const RECOVERY_VARIABLE_KEYS = Object.freeze({ + 'pull-request-link-restored': Object.freeze([]), + 'pull-request-link-base-retained': Object.freeze([]), + 'pull-request-link-reference-retained': Object.freeze([]), + 'pull-request-link-base-and-reference-retained': Object.freeze([]), + 'managed-branch-enrichment-failed': Object.freeze(['branchName']), + 'inactivity-explanation-failed': Object.freeze(['issueNumber']), +}); +function normalizeApplicationErrorRecovery(recovery) { + if (!recovery) + return undefined; + if (!exports.APPLICATION_ERROR_RECOVERY_IDS.includes(recovery.id)) { + throw new TypeError('Application error recovery ID is invalid.'); + } + const variables = recovery.variables; + const actualKeys = Object.keys(variables).sort(); + const expectedKeys = [...RECOVERY_VARIABLE_KEYS[recovery.id]].sort(); + if (actualKeys.length !== expectedKeys.length + || actualKeys.some((key, index) => key !== expectedKeys[index])) { + throw new TypeError(`Application error recovery variables are invalid for ${recovery.id}.`); + } + if (recovery.id === 'managed-branch-enrichment-failed' + && (typeof variables.branchName !== 'string' + || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/u.test(variables.branchName))) { + throw new TypeError('Application error recovery branch name is invalid.'); + } + if (recovery.id === 'inactivity-explanation-failed' + && (typeof variables.issueNumber !== 'number' + || !Number.isSafeInteger(variables.issueNumber) + || variables.issueNumber < 1)) { + throw new TypeError('Application error recovery issue number is invalid.'); + } + return Object.freeze({ + id: recovery.id, + variables: Object.freeze({ ...variables }), + }); +} /***/ }), @@ -72749,7 +72954,7 @@ exports.renderCatalogMessage = renderCatalogMessage; exports.catalogPlaceholders = catalogPlaceholders; exports.catalogPluralCategories = catalogPluralCategories; const locale_1 = __nccwpck_require__(15386); -exports.MESSAGE_CATALOG_VERSION = '1'; +exports.MESSAGE_CATALOG_VERSION = '2'; exports.CATALOG_PLURAL_CATEGORIES = Object.freeze([ 'zero', 'one', @@ -77239,6 +77444,7 @@ function logError(message, metadata) { action: message.action, retainedState: message.retainedState, correlationId: message.correlationId, + ...(message.recovery ? { recovery: message.recovery } : {}), }, }); emitLog({ level: 'error', message: sanitized, timestamp: Date.now(), metadata: safeMetadata }, console.error); diff --git a/docs/configuration.mdx b/docs/configuration.mdx index 9c80ff2cd..fef5713c3 100644 --- a/docs/configuration.mdx +++ b/docs/configuration.mdx @@ -36,7 +36,7 @@ Copilot provides extensive configuration options to customize your workflow. Use Addressed comments are interpreted in the effective surface locale with at most one agent call. Copilot never edits the human-authored source comment. When translation is needed, the useful bot response includes a collapsed, escaped copy of the interpreted request and original text. Unaddressed comments remain inert. Commands, paths, refs, URLs, identifiers, machine-readable fields, and operational logs are not localized. See [Agent CLI configuration](/agents/cli-configuration). - Repository-aware `copilot doctor` output uses `repository-locale`, not the issue or pull-request overrides. English is the default and atomic fallback; Spanish exact/base variants use reviewed bundled copy, while any other valid BCP-47 locale requires one complete schema-constrained catalog response from the configured planner/language agent. Raw provider and credential diagnostics are replaced with semantic state and recovery copy. Setup remains English while it creates this profile. + Repository-aware `copilot doctor`, local command errors, Job Summaries, and Action completion errors use `repository-locale`, not the issue or pull-request overrides. English is the default and atomic fallback; Spanish exact/base variants use reviewed bundled copy, while any other valid BCP-47 locale requires one complete schema-constrained catalog response from the configured planner/language agent. Error views localize impact, recovery, retained state, and retry guidance while preserving stable codes and correlation references; raw producer or provider messages are not replayed. Setup and CLI failures that occur before a repository profile is available remain in English. - `ai-ignore-files`: Comma-separated list of paths to ignore for AI operations (e.g. progress detection, Bugbot; not used for PR description, where the agent computes the diff in the workspace). - `bugbot-severity`: Minimum severity for Bugbot findings to report: `info`, `low`, `medium`, or `high` (default: `low`). Findings below this threshold are not posted on the issue or PR. - `bugbot-comment-limit`: Maximum number of findings to publish as individual issue comments or inline PR review comments (default: `20`). Extra findings are summarized in the PR review body or in one issue-only overflow comment. Clamped between 1 and 200. diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index 5b2537e93..3909633c8 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -143,10 +143,17 @@ closed `ApplicationError` contract: one of 20 semantic codes, a derived category and retry decision, impact, recommended action, retained-state summary, and request correlation UUID. A raw cause is ECMAScript-private, never serialized or logged, and cannot enter `Result.errors`. The generic Job -Summary projects the stable kind/code/retry/reference through its repository- -locale catalog and does not replay producer error prose. CLI and early boundary -failures retain the safe default English error view until repository-aware -presentation is available. +Summary, repository-aware CLI, deployment completion, and ordinary Action +completion project one shared code-specific error catalog. Stable codes and +references remain machine-readable; labels, impact, action, retained state, and +retry guidance follow `repository-locale`, and producer error prose is not +replayed. The pre-repository CLI boundary uses English; the early GitHub Action +boundary can use bundled English or Spanish from its validated input and falls +back atomically to English when dynamic localization is not yet available. +Partial operations select a closed recovery descriptor with only validated +branch-name or issue-number variables. This preserves exact rollback and +retained-state guidance in every locale without permitting feature code to +override presentation with arbitrary strings. The main-run setup path follows that rule end to end. Its action boundary copies and freezes a `SetupExecutionContext`, setup returns a discriminated result, and @@ -371,5 +378,5 @@ machine event/state/error values stable, and projects routine results as compact localized aggregate counts. Only failed results receive an expanded diagnostic section. Internal result names, `Result.steps`, and arbitrary error messages remain in logs or machine evidence; they are never replayed into the summary. Failures -show category-localized impact, action, and retained-state guidance beside the -stable code, retry decision, and correlation reference. +show code-specific localized impact, action, and retained-state guidance beside +the stable code, retry decision, and correlation reference. diff --git a/docs/features.mdx b/docs/features.mdx index 2b9745089..887f88966 100644 --- a/docs/features.mdx +++ b/docs/features.mdx @@ -133,7 +133,7 @@ Codex is the default runtime for the repository's AI feature paths. OpenCode and | **Think / reasoning** | Issue/PR comment pipeline; single action `think_action` | Deep code analysis and change proposals (configured agent CLI). On GitHub, returns one sanitized, correlated semantic answer to an explicit `/copilot` command or exact bot mention; Think itself has no comment mutation capability. In the CLI, it prints the answer locally and `--issue` is optional context. | | **Explicit Copilot commands** | Issue and PR comments | `/copilot help`, `/copilot plan`, `/copilot clarify`, `/copilot estimate`, `/copilot test-plan`, `/copilot explain`, `/copilot diagnose`, `/copilot analyze`, `/copilot status`, `/copilot review`, `/copilot findings`, `/copilot recheck`, `/copilot fix`, `/copilot dismiss`, `/copilot remember`, `/copilot implement`, and `/copilot sync-branch` provide a bounded, predictable interface. | | **Branch synchronization** | All-branch push observer; issue/PR command | Detection is agent-free. An authorized command merges parent into working branch, invokes the fixer only for eligible Git conflicts, validates the prepared merge, runs configured checks, revalidates remote heads, pushes, and reports the outcome. | -| **Repository language and request adaptation** | Generated GitHub UI; addressed issue/PR comments | Uses `repository-locale` (`en-US` by default), with optional inheriting `issues-locale` and `pull-requests-locale` overrides. An addressed foreign-language request is safely interpreted once; its source comment is never edited, and translation context appears only with the useful bot response. Unaddressed human and automated comments are inert. | +| **Repository language and request adaptation** | Generated GitHub UI; addressed issue/PR comments; repository-aware CLI | Uses `repository-locale` (`en-US` by default), with optional inheriting `issues-locale` and `pull-requests-locale` overrides. An addressed foreign-language request is safely interpreted once; its source comment is never edited, and translation context appears only with the useful bot response. Job Summary, Action completion, deployment, doctor, and local error recovery use complete locale catalogs while codes and references remain stable. Unaddressed human and automated comments are inert. | | **AI PR description** | Pull request pipeline | Uses `.github/pull_request_template.md` as guidance to write a concise body from optional issue context and the branch diff (configured agent CLI). | | **Copilot** | CLI `copilot do` | Code analysis and file edits via the configured agent runtime. | | **Recommend steps** | Single action / CLI | Suggests implementation steps from the issue description (configured agent CLI). | diff --git a/docs/security-operations/operations/error-reference.mdx b/docs/security-operations/operations/error-reference.mdx index cc7c2b5de..665dd8069 100644 --- a/docs/security-operations/operations/error-reference.mdx +++ b/docs/security-operations/operations/error-reference.mdx @@ -5,10 +5,12 @@ description: Stable semantic failure codes, user impact, and recovery actions. # Application error reference -Every failure presentation uses the same order: impact, safe cause and code, -recommended action, retained state, then a correlation UUID. Search the Job -Summary and sanitized logs using that UUID; it is safe to include in a support -request. Do not include credentials or private provider output. +Every failure presentation uses the same order: impact, stable error code, +recommended action, retained state, retryability, then a correlation UUID. +Search the Job Summary and sanitized logs using that UUID; it is safe to include +in a support request. Do not include credentials or private provider output. +Repository-aware views use the configured repository locale and default to +English. Error codes and references never translate. | Code | Meaning | Action | | --- | --- | --- | @@ -23,6 +25,8 @@ request. Do not include credentials or private provider output. | `provider.contract-invalid` | A provider response could not be interpreted safely. | Review the integration before retrying. | | `agent.policy-rejected` | Fixed policy rejected the agent execution plan. | Use an allowed agent configuration. | | `agent.failed` | An admitted agent did not produce a usable result. | Inspect sanitized agent status and retry if appropriate. | +| `locale.output-invalid` | Generated product content violated its target-locale contract. | Retry with a provider compatible with the configured repository locale. | +| `locale.translation-failed` | An addressed request could not be safely adapted to the target locale. | Rephrase the request or retry when the language provider is available. | | `validation.invalid-input` | Entrypoint input is malformed. | Correct the input and retry. | | `workflow.invalid-event` | The event cannot start the requested workflow. | Start from a supported event or surface. | | `workflow.stale` | A newer durable state superseded this invocation. | Inspect current state and start a fresh invocation only if needed. | @@ -35,6 +39,11 @@ Retryability is determined by the code. A specific operation can make a normally retryable failure terminal, but presentation never turns a terminal code into a retry recommendation. +Some partial operations add a closed recovery variant, for example to name a +retained managed branch or a closed issue whose final explanation was not +published. These variants use reviewed catalog templates and only bounded, +validated variables; feature code cannot replace them with arbitrary prose. + Provider exceptions are not diagnostics for public surfaces. They remain a private in-memory cause even if a catch block interpolates or assigns the value to another local variable; the architecture guard follows those derived values diff --git a/docs/single-actions/workflow-and-cli.mdx b/docs/single-actions/workflow-and-cli.mdx index bea38a2e5..b9acaaa3e 100644 --- a/docs/single-actions/workflow-and-cli.mdx +++ b/docs/single-actions/workflow-and-cli.mdx @@ -211,6 +211,12 @@ Checks the setup PAT, selected workflow templates, Repository Variables, Secret The complete human-readable report uses the configured repository locale and defaults to English (`en-US`). English and Spanish use reviewed bundled catalogs with exact or compatible-base matching. Any other valid BCP-47 locale uses one schema-constrained planner/language-agent request for the complete report catalog. If that request is unavailable or invalid, the whole report falls back atomically to English—never a mixture of translated headings and English diagnostics. Check IDs, status enums, branch names, credential names, and other machine facts remain stable. Raw provider and credential-health messages are not echoed into the report. +The same rule applies to semantic errors from repository-aware local commands: +the terminal localizes impact, action, retained state, retryability, and labels as +one catalog while leaving the error code and correlation reference stable. The +original producer message is not displayed. Failures raised before repository +configuration can be resolved use the safe English default. + This reduced two-check fixture shows the default English shape: ```text diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 3cf9302fb..1cab90db9 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -10,11 +10,11 @@ debt or convert unknown historic intent into a design decision. | Capability ID | Status | Scope | Primary SDD | Evidence | |---|---|---|---|---| -| `github-communication-experience` | Proposed | 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) + 1 companion | 160 paths · 2026-09-15 | +| `github-communication-experience` | Proposed | 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) + 1 companion | 164 paths · 2026-09-15 | | `release-orchestration` | Implemented | Release and hotfix promotion, publication, reconciliation, and durable recovery | [Configurable production-first release orchestration](./configurable-release-orchestration.md) + 2 companion | 52 paths · 2026-09-14 | | `merge-queue-readiness` | Implemented | Fail-closed validation of required checks and merge-group workflow support | [Merge queue readiness and effective target rules](./merge-queue-readiness.md) | 24 paths · 2026-09-15 | | `bugbot-review-state-reconciliation` | Implemented | Reconcile review snapshots, findings, threads, comments, and check conclusions | [Bugbot review-state reconciliation](./bugbot-review-state-reconciliation.md) | 56 paths · 2026-09-15 | -| `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 | 82 paths · 2026-09-15 | +| `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 | 84 paths · 2026-09-15 | | `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-09-15 | | `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) + 1 companion | 51 paths · 2026-09-14 | | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 21 paths · 2026-09-13 | @@ -23,7 +23,7 @@ debt or convert unknown historic intent into a design decision. | `branch-synchronization` | As-built baseline | Observe parent drift and safely merge a parent branch into a linked working branch | [Branch synchronization and conflict recovery](./branch-synchronization-and-conflict-recovery.md) | 15 paths · 2026-09-11 | | `pull-request-lifecycle` | Implemented | Enrich linked and unlinked pull requests with safe issue linkage, projects, metadata, reviewers, concise descriptions, and distinct workflow evidence | [Pull request lifecycle and enrichment](./pull-request-lifecycle-and-enrichment.md) | 38 paths · 2026-09-15 | | `agent-runtime` | Implemented | Resolve, provision, authenticate, authorize, and execute only the agent roles reachable by a run | [Agent runtime, provider, model, and role routing](./agent-runtime-provider-and-model-routing.md) + 1 companion | 51 paths · 2026-09-12 | -| `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 29 paths · 2026-09-15 | +| `cli-and-single-actions` | As-built baseline | Expose bounded local commands and workflow-dispatched operations through the shared application core | [CLI and single-action execution](./cli-and-single-action-execution.md) | 33 paths · 2026-09-15 | ## Evidence map @@ -34,8 +34,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/semantic-github-publication-and-notification.md`](./semantic-github-publication-and-notification.md) · [`specs/repository-locale-and-localization.md`](./repository-locale-and-localization.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_pull_request.yml`](../.github/workflows/copilot_pull_request.yml) · [`.github/workflows/copilot_pull_request_review_state.yml`](../.github/workflows/copilot_pull_request_review_state.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_close_inactive_issues.yml`](../.github/workflows/copilot_close_inactive_issues.yml) · [`.github/workflows/copilot_deployment_orchestration.yml`](../.github/workflows/copilot_deployment_orchestration.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/cli.ts`](../src/cli.ts) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/data/model/locale.ts`](../src/data/model/locale.ts) · [`src/actions/github_action_locale_inputs.ts`](../src/actions/github_action_locale_inputs.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/policies/resolved_message_catalog_policy.ts`](../src/application/policies/resolved_message_catalog_policy.ts) · [`src/application/policies/action_summary_message_catalog.ts`](../src/application/policies/action_summary_message_catalog.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/inactivity_message_catalog.ts`](../src/application/policies/inactivity_message_catalog.ts) · [`src/application/policies/inactivity_notification_policy.ts`](../src/application/policies/inactivity_notification_policy.ts) · [`src/application/policies/merge_queue_message_catalog.ts`](../src/application/policies/merge_queue_message_catalog.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/application/policies/setup_doctor_report_policy.ts`](../src/application/policies/setup_doctor_report_policy.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/policies/comment_translation_policy.ts`](../src/application/policies/comment_translation_policy.ts) · [`src/application/usecases/steps/common/think_request_policy.ts`](../src/application/usecases/steps/common/think_request_policy.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/think_answer_workflow.ts`](../src/application/usecases/steps/common/think_answer_workflow.ts) · [`src/application/usecases/steps/common/think_use_case.ts`](../src/application/usecases/steps/common/think_use_case.ts) · [`src/application/usecases/comment_automation_use_case.ts`](../src/application/usecases/comment_automation_use_case.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/domain/github_publication.ts`](../src/domain/github_publication.ts) · [`src/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/issue_use_case.ts`](../src/application/usecases/issue_use_case.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/steps/issue/answer_issue_help_use_case.ts`](../src/application/usecases/steps/issue/answer_issue_help_use_case.ts) · [`src/application/usecases/steps/issue/answer_issue_help_workflow.ts`](../src/application/usecases/steps/issue/answer_issue_help_workflow.ts) · [`src/application/ports/issue_lifecycle_ports.ts`](../src/application/ports/issue_lifecycle_ports.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/actions/local_action.ts`](../src/actions/local_action.ts) · [`src/actions/local_action_output.ts`](../src/actions/local_action_output.ts) · [`src/cli/commands/think.ts`](../src/cli/commands/think.ts) · [`src/cli/commands/think_command_handler.ts`](../src/cli/commands/think_command_handler.ts) · [`src/infrastructure/composition/local_action_composition_root.ts`](../src/infrastructure/composition/local_action_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/issue_use_case_composition_root.ts`](../src/infrastructure/composition/issue_use_case_composition_root.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/deployment_message_catalog.ts`](../src/application/policies/deployment_message_catalog.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_use_case.ts`](../src/application/usecases/actions/close_inactive_issues_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_workflow.ts`](../src/application/usecases/actions/close_inactive_issues_workflow.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/infrastructure/composition/issue_inactivity_composition_root.ts`](../src/infrastructure/composition/issue_inactivity_composition_root.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`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/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/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/usecases/actions/recommend_steps_workflow.ts`](../src/application/usecases/actions/recommend_steps_workflow.ts) · [`src/application/usecases/actions/check_progress_workflow.ts`](../src/application/usecases/actions/check_progress_workflow.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) -- Tests: [`src/domain/__tests__/locale.test.ts`](../src/domain/__tests__/locale.test.ts) · [`src/domain/__tests__/message_catalog.test.ts`](../src/domain/__tests__/message_catalog.test.ts) · [`src/actions/__tests__/configuration_builders.test.ts`](../src/actions/__tests__/configuration_builders.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/policies/__tests__/comment_translation_policy.test.ts`](../src/application/policies/__tests__/comment_translation_policy.test.ts) · [`src/application/policies/__tests__/action_summary_message_catalog.test.ts`](../src/application/policies/__tests__/action_summary_message_catalog.test.ts) · [`src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts`](../src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts) · [`src/prompts/__tests__/localize_message_catalog.test.ts`](../src/prompts/__tests__/localize_message_catalog.test.ts) · [`src/domain/__tests__/github_publication.test.ts`](../src/domain/__tests__/github_publication.test.ts) · [`src/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_policy.test.ts) · [`src/application/policies/__tests__/publication_message_catalog.test.ts`](../src/application/policies/__tests__/publication_message_catalog.test.ts) · [`src/application/policies/__tests__/semantic_result_publication_policy.test.ts`](../src/application/policies/__tests__/semantic_result_publication_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__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/policies/__tests__/inactivity_message_catalog.test.ts`](../src/application/policies/__tests__/inactivity_message_catalog.test.ts) · [`src/application/policies/__tests__/inactivity_notification_policy.test.ts`](../src/application/policies/__tests__/inactivity_notification_policy.test.ts) · [`src/application/policies/__tests__/setup_doctor_message_catalog.test.ts`](../src/application/policies/__tests__/setup_doctor_message_catalog.test.ts) · [`src/application/policies/__tests__/setup_doctor_report_policy.test.ts`](../src/application/policies/__tests__/setup_doctor_report_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts`](../src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/deployment_message_catalog.test.ts`](../src/application/policies/__tests__/deployment_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.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/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.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__/deployment_presentation_policy.test.ts`](../src/application/policies/__tests__/deployment_presentation_policy.test.ts) · [`src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts`](../src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/think_request_policy.test.ts`](../src/application/usecases/steps/common/__tests__/think_request_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/think_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/think_use_case.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/steps/common/__tests__/status_card_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts) · [`src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/actions/__tests__/local_action.test.ts`](../src/actions/__tests__/local_action.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/commands/__tests__/think_command_handler.test.ts`](../src/cli/commands/__tests__/think_command_handler.test.ts) · [`src/infrastructure/composition/__tests__/local_action_composition_root.test.ts`](../src/infrastructure/composition/__tests__/local_action_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.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/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts`](../src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts) · [`src/application/usecases/actions/__tests__/check_progress_use_case.test.ts`](../src/application/usecases/actions/__tests__/check_progress_use_case.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/domain/locale.ts`](../src/domain/locale.ts) · [`src/domain/message_catalog.ts`](../src/domain/message_catalog.ts) · [`src/data/model/locale.ts`](../src/data/model/locale.ts) · [`src/actions/github_action_locale_inputs.ts`](../src/actions/github_action_locale_inputs.ts) · [`src/application/ports/message_catalog_ports.ts`](../src/application/ports/message_catalog_ports.ts) · [`src/application/policies/resolved_message_catalog_policy.ts`](../src/application/policies/resolved_message_catalog_policy.ts) · [`src/application/policies/action_summary_message_catalog.ts`](../src/application/policies/action_summary_message_catalog.ts) · [`src/application/policies/branch_sync_message_catalog.ts`](../src/application/policies/branch_sync_message_catalog.ts) · [`src/application/policies/inactivity_message_catalog.ts`](../src/application/policies/inactivity_message_catalog.ts) · [`src/application/policies/inactivity_notification_policy.ts`](../src/application/policies/inactivity_notification_policy.ts) · [`src/application/policies/merge_queue_message_catalog.ts`](../src/application/policies/merge_queue_message_catalog.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/application/policies/setup_doctor_report_policy.ts`](../src/application/policies/setup_doctor_report_policy.ts) · [`src/application/usecases/localization/resolve_message_catalog_use_case.ts`](../src/application/usecases/localization/resolve_message_catalog_use_case.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/usecases/setup/merge_queue_readiness_use_case.ts`](../src/application/usecases/setup/merge_queue_readiness_use_case.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/policies/comment_translation_policy.ts`](../src/application/policies/comment_translation_policy.ts) · [`src/application/usecases/steps/common/think_request_policy.ts`](../src/application/usecases/steps/common/think_request_policy.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/think_answer_workflow.ts`](../src/application/usecases/steps/common/think_answer_workflow.ts) · [`src/application/usecases/steps/common/think_use_case.ts`](../src/application/usecases/steps/common/think_use_case.ts) · [`src/application/usecases/comment_automation_use_case.ts`](../src/application/usecases/comment_automation_use_case.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/domain/github_publication.ts`](../src/domain/github_publication.ts) · [`src/application/policies/publication_identity_policy.ts`](../src/application/policies/publication_identity_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/issue_use_case.ts`](../src/application/usecases/issue_use_case.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/steps/issue/answer_issue_help_use_case.ts`](../src/application/usecases/steps/issue/answer_issue_help_use_case.ts) · [`src/application/usecases/steps/issue/answer_issue_help_workflow.ts`](../src/application/usecases/steps/issue/answer_issue_help_workflow.ts) · [`src/application/ports/issue_lifecycle_ports.ts`](../src/application/ports/issue_lifecycle_ports.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/actions/local_action.ts`](../src/actions/local_action.ts) · [`src/actions/local_action_output.ts`](../src/actions/local_action_output.ts) · [`src/cli/commands/think.ts`](../src/cli/commands/think.ts) · [`src/cli/commands/think_command_handler.ts`](../src/cli/commands/think_command_handler.ts) · [`src/infrastructure/composition/local_action_composition_root.ts`](../src/infrastructure/composition/local_action_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/infrastructure/composition/issue_use_case_composition_root.ts`](../src/infrastructure/composition/issue_use_case_composition_root.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/application/policies/action_summary_policy.ts`](../src/application/policies/action_summary_policy.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_policy.ts) · [`src/application/policies/branch_sync_notification_policy.ts`](../src/application/policies/branch_sync_notification_policy.ts) · [`src/application/policies/bugbot_message_catalog.ts`](../src/application/policies/bugbot_message_catalog.ts) · [`src/application/policies/deployment_message_catalog.ts`](../src/application/policies/deployment_message_catalog.ts) · [`src/application/usecases/actions/observe_branch_sync_use_case.ts`](../src/application/usecases/actions/observe_branch_sync_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_use_case.ts`](../src/application/usecases/actions/close_inactive_issues_use_case.ts) · [`src/application/usecases/actions/close_inactive_issues_workflow.ts`](../src/application/usecases/actions/close_inactive_issues_workflow.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/infrastructure/composition/issue_inactivity_composition_root.ts`](../src/infrastructure/composition/issue_inactivity_composition_root.ts) · [`src/application/policies/bugbot_review_presentation_policy.ts`](../src/application/policies/bugbot_review_presentation_policy.ts) · [`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/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/deployment_presentation_policy.ts`](../src/application/policies/deployment_presentation_policy.ts) · [`src/application/usecases/actions/recommend_steps_workflow.ts`](../src/application/usecases/actions/recommend_steps_workflow.ts) · [`src/application/usecases/actions/check_progress_workflow.ts`](../src/application/usecases/actions/check_progress_workflow.ts) · [`src/data/repository/issue/issue_content_repository.ts`](../src/data/repository/issue/issue_content_repository.ts) +- Tests: [`src/domain/__tests__/locale.test.ts`](../src/domain/__tests__/locale.test.ts) · [`src/domain/__tests__/message_catalog.test.ts`](../src/domain/__tests__/message_catalog.test.ts) · [`src/actions/__tests__/configuration_builders.test.ts`](../src/actions/__tests__/configuration_builders.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/policies/__tests__/comment_translation_policy.test.ts`](../src/application/policies/__tests__/comment_translation_policy.test.ts) · [`src/application/policies/__tests__/action_summary_message_catalog.test.ts`](../src/application/policies/__tests__/action_summary_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_message_catalog.test.ts`](../src/application/policies/__tests__/application_error_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts`](../src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts) · [`src/prompts/__tests__/localize_message_catalog.test.ts`](../src/prompts/__tests__/localize_message_catalog.test.ts) · [`src/domain/__tests__/github_publication.test.ts`](../src/domain/__tests__/github_publication.test.ts) · [`src/application/policies/__tests__/publication_identity_policy.test.ts`](../src/application/policies/__tests__/publication_identity_policy.test.ts) · [`src/application/policies/__tests__/publication_message_catalog.test.ts`](../src/application/policies/__tests__/publication_message_catalog.test.ts) · [`src/application/policies/__tests__/semantic_result_publication_policy.test.ts`](../src/application/policies/__tests__/semantic_result_publication_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__/branch_sync_notification_policy.test.ts`](../src/application/policies/__tests__/branch_sync_notification_policy.test.ts) · [`src/application/policies/__tests__/inactivity_message_catalog.test.ts`](../src/application/policies/__tests__/inactivity_message_catalog.test.ts) · [`src/application/policies/__tests__/inactivity_notification_policy.test.ts`](../src/application/policies/__tests__/inactivity_notification_policy.test.ts) · [`src/application/policies/__tests__/setup_doctor_message_catalog.test.ts`](../src/application/policies/__tests__/setup_doctor_message_catalog.test.ts) · [`src/application/policies/__tests__/setup_doctor_report_policy.test.ts`](../src/application/policies/__tests__/setup_doctor_report_policy.test.ts) · [`src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts`](../src/application/usecases/actions/__tests__/observe_branch_sync_use_case.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts`](../src/application/usecases/setup/__tests__/merge_queue_readiness_use_case.test.ts) · [`src/application/policies/__tests__/bugbot_message_catalog.test.ts`](../src/application/policies/__tests__/bugbot_message_catalog.test.ts) · [`src/application/policies/__tests__/deployment_message_catalog.test.ts`](../src/application/policies/__tests__/deployment_message_catalog.test.ts) · [`src/application/policies/__tests__/bugbot_review_presentation_policy.test.ts`](../src/application/policies/__tests__/bugbot_review_presentation_policy.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/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/dismiss_bugbot_findings_use_case.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__/deployment_presentation_policy.test.ts`](../src/application/policies/__tests__/deployment_presentation_policy.test.ts) · [`src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts`](../src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/comment_language_translation_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/think_request_policy.test.ts`](../src/application/usecases/steps/common/__tests__/think_request_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/think_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/think_use_case.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/steps/common/__tests__/status_card_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/status_card_publication_workflow.test.ts) · [`src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts`](../src/application/usecases/steps/common/__tests__/reply_publication_workflow.test.ts) · [`src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/answer_issue_help_use_case.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/actions/__tests__/local_action.test.ts`](../src/actions/__tests__/local_action.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/commands/__tests__/think_command_handler.test.ts`](../src/cli/commands/__tests__/think_command_handler.test.ts) · [`src/infrastructure/composition/__tests__/local_action_composition_root.test.ts`](../src/infrastructure/composition/__tests__/local_action_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts`](../src/infrastructure/composition/__tests__/issue_use_case_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.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/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/tooling/__tests__/validate_workflow_contract.test.ts`](../src/tooling/__tests__/validate_workflow_contract.test.ts) · [`src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts`](../src/application/usecases/actions/__tests__/recommend_steps_use_case.test.ts) · [`src/application/usecases/actions/__tests__/check_progress_use_case.test.ts`](../src/application/usecases/actions/__tests__/check_progress_use_case.test.ts) · [`src/application/usecases/__tests__/comment_automation_use_case.test.ts`](../src/application/usecases/__tests__/comment_automation_use_case.test.ts) - User documentation: [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/configuration-checklist.mdx`](../docs/configuration-checklist.mdx) · [`docs/features.mdx`](../docs/features.mdx) · [`docs/issues/configuration.mdx`](../docs/issues/configuration.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/issues/notifications-and-auto-close.mdx`](../docs/issues/notifications-and-auto-close.mdx) · [`docs/issues/branch-synchronization.mdx`](../docs/issues/branch-synchronization.mdx) · [`docs/issues/type/feature.mdx`](../docs/issues/type/feature.mdx) · [`docs/issues/type/bugfix.mdx`](../docs/issues/type/bugfix.mdx) · [`docs/issues/type/docs.mdx`](../docs/issues/type/docs.mdx) · [`docs/issues/type/chore.mdx`](../docs/issues/type/chore.mdx) · [`docs/issues/type/hotfix.mdx`](../docs/issues/type/hotfix.mdx) · [`docs/issues/type/release.mdx`](../docs/issues/type/release.mdx) · [`docs/issues/deployment-orchestration.mdx`](../docs/issues/deployment-orchestration.mdx) · [`docs/pull-requests/configuration.mdx`](../docs/pull-requests/configuration.mdx) · [`docs/pull-requests/capabilities.mdx`](../docs/pull-requests/capabilities.mdx) · [`docs/pull-requests/workflow-setup.mdx`](../docs/pull-requests/workflow-setup.mdx) · [`docs/pull-requests/examples.mdx`](../docs/pull-requests/examples.mdx) · [`docs/bugbot/configuration.mdx`](../docs/bugbot/configuration.mdx) · [`docs/bugbot/finding-publication.mdx`](../docs/bugbot/finding-publication.mdx) · [`docs/bugbot/detection.mdx`](../docs/bugbot/detection.mdx) · [`docs/bugbot/how-it-works.mdx`](../docs/bugbot/how-it-works.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/bugbot/examples.mdx`](../docs/bugbot/examples.mdx) · [`docs/single-actions/configuration.mdx`](../docs/single-actions/configuration.mdx) · [`docs/single-actions/available-actions.mdx`](../docs/single-actions/available-actions.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) · [`docs/single-actions/examples.mdx`](../docs/single-actions/examples.mdx) · [`docs/security-operations/security/prompt-injection.mdx`](../docs/security-operations/security/prompt-injection.mdx) · [`docs/security-operations/operations/verification.mdx`](../docs/security-operations/operations/verification.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) ### `release-orchestration` — Configurable production-first release orchestration @@ -78,8 +78,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/execution-admission-queue-and-publication.md`](./execution-admission-queue-and-publication.md) · [`specs/execution-error-and-context-hardening.md`](./execution-error-and-context-hardening.md) · [`specs/execution-boundary-closure-audit.md`](./execution-boundary-closure-audit.md) · [`specs/push-and-single-action-context-hardening.md`](./push-and-single-action-context-hardening.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.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) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) - Entrypoints: [`src/actions/github_action.ts`](../src/actions/github_action.ts) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) -- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-coverage-budgets.cjs`](../scripts/validate-coverage-budgets.cjs) · [`src/actions/main_run_lifecycle.ts`](../src/actions/main_run_lifecycle.ts) · [`src/actions/main_run_route.ts`](../src/actions/main_run_route.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/errors/application_error.ts`](../src/application/errors/application_error.ts) · [`src/application/errors/application_error_context.ts`](../src/application/errors/application_error_context.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_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/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/policies/github_execution_admission_policy.ts`](../src/application/policies/github_execution_admission_policy.ts) · [`src/application/policies/workflow_queue_policy.ts`](../src/application/policies/workflow_queue_policy.ts) · [`src/application/usecases/actions/lifecycle_synchronization_context.ts`](../src/application/usecases/actions/lifecycle_synchronization_context.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/check_permissions_workflow.ts`](../src/application/usecases/steps/common/check_permissions_workflow.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/application/usecases/steps/common/project_content_link_workflow.ts`](../src/application/usecases/steps/common/project_content_link_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/common/store_configuration_use_case.ts`](../src/application/usecases/steps/common/store_configuration_use_case.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_state_composition_root.ts`](../src/infrastructure/composition/lifecycle_state_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) -- Tests: [`src/tooling/__tests__/coverage_budget_validator.test.ts`](../src/tooling/__tests__/coverage_budget_validator.test.ts) · [`src/__tests__/api.test.ts`](../src/__tests__/api.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.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_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_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/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_synchronization_context.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/actions/__tests__/lifecycle_event_replay.integration.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts) · [`src/architecture/__tests__/execution_import_ratchet.test.ts`](../src/architecture/__tests__/execution_import_ratchet.test.ts) · [`src/architecture/__tests__/raw_error_logging_boundaries.test.ts`](../src/architecture/__tests__/raw_error_logging_boundaries.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts) · [`src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts`](../src/application/usecases/steps/common/__tests__/shared_capability_context_projection.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/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/__tests__/single_action_workflow.test.ts`](../src/application/usecases/__tests__/single_action_workflow.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts`](../src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts) · [`src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts`](../src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts) +- Core code: [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`scripts/validate-coverage-budgets.cjs`](../scripts/validate-coverage-budgets.cjs) · [`src/actions/main_run_lifecycle.ts`](../src/actions/main_run_lifecycle.ts) · [`src/actions/main_run_route.ts`](../src/actions/main_run_route.ts) · [`src/actions/github_action_completion.ts`](../src/actions/github_action_completion.ts) · [`src/api.ts`](../src/api.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/errors/application_error.ts`](../src/application/errors/application_error.ts) · [`src/application/errors/application_error_context.ts`](../src/application/errors/application_error_context.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_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/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/policies/github_execution_admission_policy.ts`](../src/application/policies/github_execution_admission_policy.ts) · [`src/application/policies/workflow_queue_policy.ts`](../src/application/policies/workflow_queue_policy.ts) · [`src/application/usecases/actions/lifecycle_synchronization_context.ts`](../src/application/usecases/actions/lifecycle_synchronization_context.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts`](../src/application/usecases/steps/commit/bugbot/bugbot_review_operation_context.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/application/usecases/steps/common/status_card_publication_workflow.ts`](../src/application/usecases/steps/common/status_card_publication_workflow.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/issue_workflow_context.ts`](../src/application/usecases/issue_workflow_context.ts) · [`src/application/usecases/pull_request_workflow_context.ts`](../src/application/usecases/pull_request_workflow_context.ts) · [`src/application/usecases/push_single_action_contexts.ts`](../src/application/usecases/push_single_action_contexts.ts) · [`src/application/usecases/steps/common/check_permissions_workflow.ts`](../src/application/usecases/steps/common/check_permissions_workflow.ts) · [`src/application/usecases/steps/common/comment_language_translation_workflow.ts`](../src/application/usecases/steps/common/comment_language_translation_workflow.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/update_title_workflow.ts`](../src/application/usecases/steps/common/update_title_workflow.ts) · [`src/application/usecases/steps/common/project_content_link_workflow.ts`](../src/application/usecases/steps/common/project_content_link_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`src/application/usecases/steps/common/store_configuration_use_case.ts`](../src/application/usecases/steps/common/store_configuration_use_case.ts) · [`src/infrastructure/composition/shared_capability_port_binding.ts`](../src/infrastructure/composition/shared_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_capability_port_binding.ts`](../src/infrastructure/composition/lifecycle_capability_port_binding.ts) · [`src/infrastructure/composition/lifecycle_state_composition_root.ts`](../src/infrastructure/composition/lifecycle_state_composition_root.ts) · [`src/infrastructure/composition/push_single_action_capability_port_binding.ts`](../src/infrastructure/composition/push_single_action_capability_port_binding.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) +- Tests: [`src/tooling/__tests__/coverage_budget_validator.test.ts`](../src/tooling/__tests__/coverage_budget_validator.test.ts) · [`src/__tests__/api.test.ts`](../src/__tests__/api.test.ts) · [`src/actions/__tests__/github_action.test.ts`](../src/actions/__tests__/github_action.test.ts) · [`src/actions/__tests__/github_action_completion.test.ts`](../src/actions/__tests__/github_action_completion.test.ts) · [`src/application/errors/__tests__/application_error.test.ts`](../src/application/errors/__tests__/application_error.test.ts) · [`src/application/policies/__tests__/application_error_message_catalog.test.ts`](../src/application/policies/__tests__/application_error_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.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_telemetry_projection_policy.test.ts`](../src/application/policies/__tests__/bugbot_telemetry_projection_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/application/usecases/actions/__tests__/lifecycle_synchronization_context.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_synchronization_context.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/actions/__tests__/lifecycle_event_replay.integration.test.ts`](../src/application/usecases/actions/__tests__/lifecycle_event_replay.integration.test.ts) · [`src/architecture/__tests__/execution_import_ratchet.test.ts`](../src/architecture/__tests__/execution_import_ratchet.test.ts) · [`src/architecture/__tests__/raw_error_logging_boundaries.test.ts`](../src/architecture/__tests__/raw_error_logging_boundaries.test.ts) · [`src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts`](../src/application/usecases/steps/commit/bugbot/__tests__/bugbot_review_operation_context.test.ts) · [`src/application/usecases/steps/common/__tests__/shared_capability_context_projection.test.ts`](../src/application/usecases/steps/common/__tests__/shared_capability_context_projection.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/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/shared_capability_port_binding.test.ts) · [`src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts`](../src/application/usecases/__tests__/issue_pull_request_context_projection.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_capability_port_binding.test.ts) · [`src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts`](../src/infrastructure/composition/__tests__/lifecycle_state_composition_root.test.ts) · [`src/application/usecases/__tests__/push_single_action_contexts.test.ts`](../src/application/usecases/__tests__/push_single_action_contexts.test.ts) · [`src/application/usecases/__tests__/single_action_workflow.test.ts`](../src/application/usecases/__tests__/single_action_workflow.test.ts) · [`src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts`](../src/infrastructure/composition/__tests__/push_single_action_capability_port_binding.test.ts) · [`src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts`](../src/application/usecases/execution/__tests__/resolve_github_execution_admission_use_case.test.ts) · [`src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts`](../src/application/usecases/workflow/__tests__/wait_for_previous_workflow_runs_use_case.test.ts) - User documentation: [`docs/overview.mdx`](../docs/overview.mdx) · [`docs/bugbot/programmatic-api.mdx`](../docs/bugbot/programmatic-api.mdx) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) · [`docs/security-operations/operations/troubleshooting.mdx`](../docs/security-operations/operations/troubleshooting.mdx) · [`docs/bugbot/quality-observability.mdx`](../docs/bugbot/quality-observability.mdx) · [`docs/bugbot/failure-scenarios.mdx`](../docs/bugbot/failure-scenarios.mdx) · [`docs/development/architecture.mdx`](../docs/development/architecture.mdx) ### `architecture-quality-hardening` — Architecture quality and scalability hardening @@ -177,8 +177,8 @@ debt or convert unknown historic intent into a design decision. - Specifications: [`specs/cli-and-single-action-execution.md`](./cli-and-single-action-execution.md) - Workflows: Not applicable for this capability. - Entrypoints: [`src/cli.ts`](../src/cli.ts) · [`src/cli/cli_program.ts`](../src/cli/cli_program.ts) · [`src/actions/local_action.ts`](../src/actions/local_action.ts) · [`src/application/usecases/single_action_use_case.ts`](../src/application/usecases/single_action_use_case.ts) -- Core code: [`src/cli/command_registry.ts`](../src/cli/command_registry.ts) · [`src/cli/commands/think.ts`](../src/cli/commands/think.ts) · [`src/cli/commands/think_command_handler.ts`](../src/cli/commands/think_command_handler.ts) · [`src/data/model/action_types.ts`](../src/data/model/action_types.ts) · [`src/data/model/single_action.ts`](../src/data/model/single_action.ts) · [`src/application/usecases/single_action_workflow.ts`](../src/application/usecases/single_action_workflow.ts) · [`src/application/usecases/steps/common/think_request_policy.ts`](../src/application/usecases/steps/common/think_request_policy.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/think_answer_workflow.ts`](../src/application/usecases/steps/common/think_answer_workflow.ts) · [`src/actions/local_action_output.ts`](../src/actions/local_action_output.ts) · [`src/infrastructure/composition/local_action_composition_root.ts`](../src/infrastructure/composition/local_action_composition_root.ts) -- Tests: [`src/cli/__tests__/cli_program.test.ts`](../src/cli/__tests__/cli_program.test.ts) · [`src/cli/__tests__/cli_entrypoint_boundaries.test.ts`](../src/cli/__tests__/cli_entrypoint_boundaries.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/commands/__tests__/think_command_handler.test.ts`](../src/cli/commands/__tests__/think_command_handler.test.ts) · [`src/actions/__tests__/local_action.test.ts`](../src/actions/__tests__/local_action.test.ts) · [`src/application/usecases/steps/common/__tests__/think_request_policy.test.ts`](../src/application/usecases/steps/common/__tests__/think_request_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/think_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/think_use_case.test.ts) · [`src/infrastructure/composition/__tests__/local_action_composition_root.test.ts`](../src/infrastructure/composition/__tests__/local_action_composition_root.test.ts) · [`src/application/usecases/__tests__/single_action_use_case.test.ts`](../src/application/usecases/__tests__/single_action_use_case.test.ts) +- Core code: [`src/cli/command_registry.ts`](../src/cli/command_registry.ts) · [`src/cli/commands/think.ts`](../src/cli/commands/think.ts) · [`src/cli/commands/think_command_handler.ts`](../src/cli/commands/think_command_handler.ts) · [`src/data/model/action_types.ts`](../src/data/model/action_types.ts) · [`src/data/model/single_action.ts`](../src/data/model/single_action.ts) · [`src/application/usecases/single_action_workflow.ts`](../src/application/usecases/single_action_workflow.ts) · [`src/application/usecases/steps/common/think_request_policy.ts`](../src/application/usecases/steps/common/think_request_policy.ts) · [`src/application/usecases/steps/common/think_workflow.ts`](../src/application/usecases/steps/common/think_workflow.ts) · [`src/application/usecases/steps/common/think_answer_workflow.ts`](../src/application/usecases/steps/common/think_answer_workflow.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/application/policies/application_error_presentation_policy.ts`](../src/application/policies/application_error_presentation_policy.ts) · [`src/actions/local_action_output.ts`](../src/actions/local_action_output.ts) · [`src/infrastructure/composition/local_action_composition_root.ts`](../src/infrastructure/composition/local_action_composition_root.ts) +- Tests: [`src/cli/__tests__/cli_program.test.ts`](../src/cli/__tests__/cli_program.test.ts) · [`src/cli/__tests__/cli_entrypoint_boundaries.test.ts`](../src/cli/__tests__/cli_entrypoint_boundaries.test.ts) · [`src/__tests__/cli.test.ts`](../src/__tests__/cli.test.ts) · [`src/cli/commands/__tests__/think_command_handler.test.ts`](../src/cli/commands/__tests__/think_command_handler.test.ts) · [`src/actions/__tests__/local_action.test.ts`](../src/actions/__tests__/local_action.test.ts) · [`src/application/policies/__tests__/application_error_message_catalog.test.ts`](../src/application/policies/__tests__/application_error_message_catalog.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/think_request_policy.test.ts`](../src/application/usecases/steps/common/__tests__/think_request_policy.test.ts) · [`src/application/usecases/steps/common/__tests__/think_use_case.test.ts`](../src/application/usecases/steps/common/__tests__/think_use_case.test.ts) · [`src/infrastructure/composition/__tests__/local_action_composition_root.test.ts`](../src/infrastructure/composition/__tests__/local_action_composition_root.test.ts) · [`src/application/usecases/__tests__/single_action_use_case.test.ts`](../src/application/usecases/__tests__/single_action_use_case.test.ts) - User documentation: [`docs/single-actions/available-actions.mdx`](../docs/single-actions/available-actions.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) · [`docs/single-actions/examples.mdx`](../docs/single-actions/examples.mdx) · [`docs/single-actions/configuration.mdx`](../docs/single-actions/configuration.mdx) · [`docs/agents/cli-commands.mdx`](../docs/agents/cli-commands.mdx) ## Maintenance contract diff --git a/specs/architecture-quality-and-scalability-hardening.md b/specs/architecture-quality-and-scalability-hardening.md index 2ae99f9e6..c2f654994 100644 --- a/specs/architecture-quality-and-scalability-hardening.md +++ b/specs/architecture-quality-and-scalability-hardening.md @@ -254,14 +254,15 @@ declared architectural progress. The exact contract is owned by [`execution-error-and-context-hardening.md`](./execution-error-and-context-hardening.md). -1. Define its closed 18-value `ApplicationErrorCode` union, derived kind and +1. Define its closed 20-value `ApplicationErrorCode` union, derived kind and retryability, safe public message, impact, recommended action, retained-state summary, UUID correlation ID, and ECMAScript-private non-serializable cause. 2. Change `Result.errors` to a readonly semantic error contract. Boundary helpers MUST map expected provider failures and normalize unexpected failures without interpolating the raw value. 3. Adapters map provider-specific status into semantic codes. Application use - cases may add product impact/action but may not inspect provider DTOs. + cases may select a closed recovery descriptor with bounded safe variables, + but may not inject presentation prose or inspect provider DTOs. 4. Structured logs accept only allowlisted semantic fields and a correlation identifier. Raw exceptions are not a logging escape hatch. 5. Add AST-based architecture checks for raw caught values in results/logs and @@ -274,9 +275,9 @@ The exact contract is owned by adapter, alias, overload, or deprecation period is implemented. Exit criteria: zero production `errors: [error]` patterns; every failure code -has presentation and retry semantics; the error policy has 100% enumerated -branch coverage; public/log fixtures contain no raw provider diagnostics; the -aggregate-import baseline is checked in and non-increasing. +and recovery descriptor has presentation and retry semantics; the error policy +has 100% enumerated branch coverage; public/log fixtures contain no raw provider +diagnostics; the aggregate-import baseline is checked in and non-increasing. ### 6.4 P0-B — deployment correctness and decomposition diff --git a/specs/catalog.json b/specs/catalog.json index 252ae8239..a26a66363 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -76,6 +76,8 @@ "src/infrastructure/composition/shared_capability_port_binding.ts", "src/architecture/github_publication_mutation_baseline.json", "src/application/policies/action_summary_policy.ts", + "src/application/policies/application_error_message_catalog.ts", + "src/application/policies/application_error_presentation_policy.ts", "src/application/policies/branch_sync_notification_policy.ts", "src/application/policies/bugbot_message_catalog.ts", "src/application/policies/deployment_message_catalog.ts", @@ -100,6 +102,8 @@ "src/actions/__tests__/github_action_completion.test.ts", "src/application/policies/__tests__/comment_translation_policy.test.ts", "src/application/policies/__tests__/action_summary_message_catalog.test.ts", + "src/application/policies/__tests__/application_error_message_catalog.test.ts", + "src/application/policies/__tests__/application_error_presentation_policy.test.ts", "src/application/usecases/localization/__tests__/resolve_message_catalog_use_case.test.ts", "src/prompts/__tests__/localize_message_catalog.test.ts", "src/domain/__tests__/github_publication.test.ts", @@ -414,6 +418,7 @@ "src/data/model/application_error.ts", "src/application/errors/application_error.ts", "src/application/errors/application_error_context.ts", + "src/application/policies/application_error_message_catalog.ts", "src/application/policies/application_error_presentation_policy.ts", "src/application/policies/bugbot_result_finding_state_projection_policy.ts", "src/application/policies/bugbot_telemetry_projection_policy.ts", @@ -451,6 +456,7 @@ "src/actions/__tests__/github_action.test.ts", "src/actions/__tests__/github_action_completion.test.ts", "src/application/errors/__tests__/application_error.test.ts", + "src/application/policies/__tests__/application_error_message_catalog.test.ts", "src/application/policies/__tests__/application_error_presentation_policy.test.ts", "src/application/policies/__tests__/bugbot_result_finding_state_projection_policy.test.ts", "src/application/policies/__tests__/bugbot_telemetry_projection_policy.test.ts", @@ -1048,6 +1054,8 @@ "src/application/usecases/steps/common/think_request_policy.ts", "src/application/usecases/steps/common/think_workflow.ts", "src/application/usecases/steps/common/think_answer_workflow.ts", + "src/application/policies/application_error_message_catalog.ts", + "src/application/policies/application_error_presentation_policy.ts", "src/actions/local_action_output.ts", "src/infrastructure/composition/local_action_composition_root.ts" ], @@ -1057,6 +1065,8 @@ "src/__tests__/cli.test.ts", "src/cli/commands/__tests__/think_command_handler.test.ts", "src/actions/__tests__/local_action.test.ts", + "src/application/policies/__tests__/application_error_message_catalog.test.ts", + "src/application/policies/__tests__/application_error_presentation_policy.test.ts", "src/application/usecases/steps/common/__tests__/think_request_policy.test.ts", "src/application/usecases/steps/common/__tests__/think_use_case.test.ts", "src/infrastructure/composition/__tests__/local_action_composition_root.test.ts", diff --git a/specs/cli-and-single-action-execution.md b/specs/cli-and-single-action-execution.md index dfaa6ee14..9dfdc6cb1 100644 --- a/specs/cli-and-single-action-execution.md +++ b/specs/cli-and-single-action-execution.md @@ -252,6 +252,12 @@ description-query and agent-query ports and returns immutable `direct-answer` data. GitHub publication is source-correlated by the shared reply reconciler; the CLI renders the same semantic answer locally using English-default, reviewed-Spanish, arbitrary dynamic, or atomic-English-fallback catalog labels. +Repository-aware local failures now use the same code-specific descriptor +catalog as GitHub completion and Job Summaries: labels and recovery guidance use +the effective repository locale, while error codes and references remain stable +machine values and producer exception prose is omitted. Argument parsing and +other failures before a repository profile can be resolved remain English by +default. ## 14. Testing strategy and numeric budget diff --git a/specs/execution-error-and-context-hardening.md b/specs/execution-error-and-context-hardening.md index d7879f278..1ab17fa8c 100644 --- a/specs/execution-error-and-context-hardening.md +++ b/specs/execution-error-and-context-hardening.md @@ -162,6 +162,8 @@ semantic contract. | `provider.contract-invalid` | provider | no | response violates the adapter contract | | `agent.policy-rejected` | agent | no | execution plan violates fixed policy | | `agent.failed` | agent | yes | an admitted agent process failed | +| `locale.output-invalid` | agent | yes | generated product content violates its target-locale contract | +| `locale.translation-failed` | agent | yes | an addressed request could not be safely adapted to the target locale | | `validation.invalid-input` | validation | no | trusted entrypoint input is malformed | | `workflow.invalid-event` | workflow | no | event cannot target the requested workflow | | `workflow.stale` | workflow | no | newer durable state superseded this event | @@ -178,9 +180,12 @@ specific product decision, but never broadened from no to yes at presentation. The class extends `Error` for standard JavaScript error semantics and has these public, readonly fields: `code`, derived `kind`, `retryable`, `impact`, `action`, -`retainedState`, and `correlationId`. Its inherited `message` is the safe public -message. `toJSON()` returns exactly those allowlisted fields plus `name` and -`message`. +`retainedState`, `correlationId`, and an optional closed `recovery` descriptor. +Its inherited `message` is the safe public message. `toJSON()` returns exactly +those allowlisted fields plus `name`, `message`, and the optional descriptor. +The descriptor ID selects reviewed code-specific recovery templates, and its +variables are an exact, bounded allowlist of validated branch names or positive +issue numbers. Application callers cannot override presentation prose directly. The raw cause is stored in an ECMAScript `#cause` private field with no public getter and is never serialized. It exists only for an in-memory debugger before @@ -526,25 +531,36 @@ remain facts copied into the relevant context after ordinary validation. ## 9. UI/UX and content contract -Every failure view follows `impact -> cause category -> action -> retained -state -> correlation ID`. GitHub annotations and terminal output use the same -semantic view model. Exit code is `0` for success/no-op, `1` for failed/blocked, -and `130` only for an explicit local user cancellation. +Every failure view follows `impact -> stable error code -> action -> retained +state -> retryability -> correlation ID`. GitHub annotations, Job Summaries, +Action completion, and terminal output use the same semantic view model and do +not interpolate producer exception prose. Exit code is `0` for success/no-op, +`1` for failed/blocked, and `130` only for an explicit local user cancellation. ```markdown ### Deployment could not continue **Impact:** No repository or release mutation was attempted. -**Cause:** The saved deployment state is newer than this event. +**Error code:** `workflow.stale` **Action:** Re-run the operation from the launcher issue. **Retained state:** The existing deployment state and published artifacts were preserved. +**Retryable:** No **Reference:** `6f173f89-96a3-4fc8-b90e-4f8f48e0e319` ``` Pending views do not show an error. Partial views name successful irreversible facts before the failure. Completed views contain no correlation ID unless it -is already part of the standard operator evidence. Existing locale resolution -and English fallback remain; codes and UUIDs are not translated. +is already part of the standard operator evidence. Repository-aware boundaries +resolve the complete code-specific descriptor set in the repository locale: +English and Spanish are bundled, arbitrary valid BCP-47 locales use the bounded +dynamic resolver, and any incomplete or invalid result falls back atomically to +English. Failures before repository configuration is available use English; +the early GitHub Action boundary MAY use a validated bundled locale input. +Labels and recovery prose are localized, while error codes and UUIDs are not. +When a partial operation needs more detail than its broad error code can convey, +it selects one closed recovery descriptor with validated variables. The entire +descriptor is translated with the surrounding catalog, so a retained branch or +issue number is preserved without mixing English prose into another locale. ## 10. Failure, recovery, and cleanup @@ -569,11 +585,12 @@ and English fallback remain; codes and UUIDs are not translated. ## 12. Observability and operational UX -Structured records contain only `code`, `kind`, `retryable`, `correlationId`, +Structured records contain only `code`, `kind`, `retryable`, `correlationId`, an +optional closed recovery descriptor with bounded validated variables, capability, operation phase, and bounded safe counts. Metrics aggregate by code -and capability, never by message. The architecture report records current, added, -and removed aggregate imports; any positive delta fails CI. No raw error sampling -is permitted in production. +and capability, never by message. The architecture report records current, +added, and removed aggregate imports; any positive delta fails CI. No raw error +sampling is permitted in production. ## 13. Compatibility, migration, rollout, and rollback @@ -589,6 +606,18 @@ is permitted in production. - Rollback means reverting the complete unreleased change. It MUST NOT add a compatibility layer, restore raw-error publication, or increase the allowlist. +Implementation evidence as of 2026-09-15: route failures return semantic +`Result` values instead of terminating through route-local `setFailed` calls. +The repository-aware CLI, generic and deployment Job Summaries, and final GitHub +Action conclusion compose the same versioned, code-specific error descriptors +into their complete surface catalog. One final boundary renders the failure and +sets the Action conclusion; producer exception messages are omitted. Early CLI +parse/configuration failures remain deterministic English, while the early +GitHub boundary can select a validated bundled English or Spanish descriptor +set and otherwise falls back atomically to English. Closed recovery variants +preserve operation-specific rollback and retained-state context without allowing +callers to inject arbitrary presentation strings. + ## 14. Testing strategy and numeric budget This SDD owns at least **130 distinct cases**: P0-A 22, P2-A through P2-F diff --git a/specs/repository-locale-and-localization.md b/specs/repository-locale-and-localization.md index f28ccc70d..0ad094c26 100644 --- a/specs/repository-locale-and-localization.md +++ b/specs/repository-locale-and-localization.md @@ -963,11 +963,19 @@ remains one authoritative English artifact while it creates the repository profile. The generic Actions Job Summary now resolves one complete catalog in the repository locale, localizes its headings and explanatory labels, preserves machine values, projects compact localized aggregate result counts and expands -only error recovery by stable error category, omits internal result names, step +only code-specific error recovery, omits internal result names, step narration, and arbitrary error text, and emits localization evidence once instead of duplicating it inside and below -the main table. Lifecycle and the remaining public surfaces are -not claimed complete by this evidence. +the main table. Repository-aware local failures, generic and deployment Job +Summaries, and final Action completion compose that same versioned descriptor +set into the complete surface catalog. Labels and recovery guidance localize; +codes and references remain stable; arbitrary producer messages never appear. +Closed recovery variants carry only validated branch-name or issue-number +variables, preserving operation-specific partial-state guidance in the same +atomic catalog locale. +Failures before repository/agent resolution use a deterministic complete +bundled or English-default catalog. Other lifecycle surfaces are not claimed +complete by this evidence. The addressed-Think follow-up removes its feature-owned comment mutation and returns the same typed `direct-answer` projection as initial issue help. GitHub diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index bda1978ba..4d73f8fb5 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -940,11 +940,19 @@ mutation inventory, locale-branch ratchet, and pseudo-plural ratchet protect these boundaries. Subsequent deployment and setup-doctor slices cover their feature-owned views, and the generic Job Summary slice resolves repository- locale copy atomically, renders aggregate result counts and expands only safe -category-localized error recovery, excludes internal result names, steps, and +code-specific localized error recovery, excludes internal result names, steps, and arbitrary error messages, and renders one localization evidence section instead of two. Other capability rows and the global numeric budget remain open and are not claimed complete by this milestone. +The shared error-presentation slice replaces route-local Action termination with +semantic results and one final locale-aware conclusion boundary. Generic and +deployment summaries, Action completion, and repository-aware CLI output +compose the same complete code-specific descriptors; stable codes and references +remain language-independent, and raw producer messages are never published. +Closed recovery variants preserve specific partial-operation context through +validated variables without introducing feature-owned presentation prose. + The initial-issue slice now enforces the §6.2 matrix at the route boundary. A normal issue with a publishable plan emits no welcome; a help/question issue returns a typed, locale-validated `direct-answer` projection that the shared diff --git a/src/actions/__tests__/common_action.test.ts b/src/actions/__tests__/common_action.test.ts index a2a22f1f9..7f6248ac9 100644 --- a/src/actions/__tests__/common_action.test.ts +++ b/src/actions/__tests__/common_action.test.ts @@ -656,7 +656,7 @@ describe('mainRun', () => { expect(execution.labels.currentIssueLabels).toEqual(['state:ready']); }); - it('calls core.setFailed when action not handled', async () => { + it('returns a semantic failure when the action route is not handled', async () => { const execution = mockExecution({ isIssue: false, isPullRequest: false, @@ -665,19 +665,23 @@ describe('mainRun', () => { const results = await runMain(execution); - expect(core.setFailed).toHaveBeenCalledWith('Action not handled.'); - expect(logInfo).toHaveBeenCalledWith('Main run finished. Results: 0, total steps: 0.'); - expect(results).toEqual([]); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(logInfo).toHaveBeenCalledWith('Main run finished. Results: 1, total steps: 0.'); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ success: false, executed: false }); + expect(results[0].errors[0]).toMatchObject({ code: 'workflow.invalid-event' }); }); - it('calls core.setFailed and returns [] when use case throws', async () => { + it('returns a semantic failure when a use case throws', async () => { const execution = mockExecution({ isPush: true }); mockCommitInvoke.mockRejectedValue(new Error('Commit failed')); const results = await runMain(execution); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Cause (workflow.failed): Main run failed.')); - expect(results).toEqual([]); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(results).toHaveLength(1); + expect(results[0]).toMatchObject({ success: false, executed: true }); + expect(results[0].errors[0]).toMatchObject({ code: 'workflow.failed', message: 'Main run failed.' }); }); it('does not expose a non-Error thrown value in the action failure', async () => { @@ -686,9 +690,10 @@ describe('mainRun', () => { const results = await runMain(execution); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Cause (workflow.failed): Main run failed.')); - expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('plain string error')); - expect(results).toEqual([]); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(results).toHaveLength(1); + expect(results[0].errors[0]).toMatchObject({ code: 'workflow.failed', message: 'Main run failed.' }); + expect(results[0].errors[0].message).not.toContain('plain string error'); }); it('propagates a canonical queue failure without exposing provider diagnostics', async () => { diff --git a/src/actions/__tests__/github_action.test.ts b/src/actions/__tests__/github_action.test.ts index 24d924025..d29bff156 100644 --- a/src/actions/__tests__/github_action.test.ts +++ b/src/actions/__tests__/github_action.test.ts @@ -330,7 +330,8 @@ describe('runGitHubAction', () => { await runGitHubAction(); expect(mockPublishInvoke).toHaveBeenCalled(); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Cause (workflow.failed): First error')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: workflow.failed')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('First error')); }); it('calls logError when INPUT_VARS_JSON is invalid JSON', async () => { @@ -372,7 +373,33 @@ describe('runGitHubActionEntry', () => { it('converts an unhandled rejection into an action failure without forcing process exit', async () => { await runGitHubActionEntry(jest.fn().mockRejectedValue(new Error('entry failed'))); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Cause (workflow.failed): GitHub Action execution failed.')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: workflow.failed')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('GitHub Action execution failed.')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('entry failed')); + }); + + it('uses a bundled repository locale for failures before execution is available', async () => { + (core.getInput as jest.Mock).mockImplementation((key: string) => + key === INPUT_KEYS.REPOSITORY_LOCALE ? 'es-MX' : ''); + + await runGitHubActionEntry(jest.fn().mockRejectedValue(new Error('entry failed'))); + + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Impacto:')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Código de error: workflow.failed')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Reintentable: Sí')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('Impact:')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('entry failed')); + }); + + it('falls back atomically to English when the early locale input is invalid', async () => { + (core.getInput as jest.Mock).mockImplementation((key: string) => + key === INPUT_KEYS.REPOSITORY_LOCALE ? 'not a locale' : ''); + + await runGitHubActionEntry(jest.fn().mockRejectedValue(new Error('entry failed'))); + + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Impact:')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: workflow.failed')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('Impacto:')); expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('entry failed')); }); }); diff --git a/src/actions/__tests__/github_action_completion.test.ts b/src/actions/__tests__/github_action_completion.test.ts index 95949834d..9581bb313 100644 --- a/src/actions/__tests__/github_action_completion.test.ts +++ b/src/actions/__tests__/github_action_completion.test.ts @@ -526,7 +526,9 @@ describe('finishGithubAction', () => { expect(mockPublishInvoke).toHaveBeenCalledWith(expect.objectContaining({ locale: 'en-US' })); expect(mockSummaryPublish).toHaveBeenCalledWith(expect.stringContaining('`provider.unavailable`')); expect(mockSummaryPublish).toHaveBeenCalledWith(expect.not.stringContaining('Title normalization failed.')); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Title normalization failed.')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: provider.unavailable')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Action: Retry when the provider is available.')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('Title normalization failed.')); }); it('fails the action for unresolved findings only when the generic policy is enabled', async () => { @@ -546,7 +548,7 @@ describe('finishGithubAction', () => { ai: new Ai('', 'model', false, [], false, 'low', 20, [], undefined, undefined, { failOnUnresolved: true }), }); await finishGithubAction(blocking, [findingResult], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith('Bugbot found 3 unresolved actionable finding(s).'); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: workflow.failed')); }); it('fails every workflow that reports an application error', async () => { @@ -559,24 +561,77 @@ describe('finishGithubAction', () => { await finishGithubAction(execution(), [failed], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Cause (agent.failed): Agent execution failed.')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: agent.failed')); expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Action: Inspect the sanitized agent status')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('Agent execution failed.')); expect(core.setFailed).toHaveBeenCalledWith(expect.stringMatching(/Reference: [0-9a-f-]{36}/)); }); + it('renders the complete failure atomically in the repository locale', async () => { + const action = Object.assign(execution(), { + locale: { repository: 'es-MX', issue: 'es-MX', pullRequest: 'es-MX' }, + }); + const failed = new Result({ + id: 'AgentBackedFeature', + success: false, + executed: true, + errors: [new ApplicationError('provider.rate-limited', 'English provider message.')], + }); + + await finishGithubAction(action, [failed], {} as never, {} as never); + + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Impacto: El proveedor limitó temporalmente la operación.')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Código de error: provider.rate-limited')); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Reintentable: Sí')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('English provider message.')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('Impact:')); + }); + + it('preserves operation-specific recovery context in the same catalog locale', async () => { + const action = Object.assign(execution(), { + locale: { repository: 'es-ES', issue: 'es-ES', pullRequest: 'es-ES' }, + }); + const failed = new Result({ + id: 'PrepareManagedBranchUseCase', + success: false, + executed: true, + errors: [new ApplicationError('provider.unavailable', 'Producer message.', { + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: 'feature/42-localized-errors' }, + }, + })], + }); + + await finishGithubAction( + action, + [failed], + {} as never, + {} as never, + undefined, + { publish: mockSummaryPublish }, + ); + + const expected = 'Se conservaron la rama feature/42-localized-errors y su parche de configuración.'; + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining(expected)); + expect(mockSummaryPublish).toHaveBeenCalledWith(expect.stringContaining(expected)); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('Producer message.')); + expect(core.setFailed).not.toHaveBeenCalledWith(expect.stringContaining('El proveedor no estaba disponible')); + }); + it('always fails unknown finding state and applies the configured policy to verification-required', async () => { const resultWith = (findingStates: Record) => new Result({ id: 'DetectPotentialProblemsUseCase', success: true, executed: true, payload: { findingStates }, }); await finishGithubAction(execution(), [resultWith(completeFindingStates({ unknown: 1 }))], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith('Bugbot could not verify 1 finding state(s).'); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: provider.contract-invalid')); jest.mocked(core.setFailed).mockClear(); const blocking = Object.assign(execution(), { ai: new Ai('', 'model', false, [], false, 'low', 20, [], undefined, undefined, { failOnUnresolved: true }), }); await finishGithubAction(blocking, [resultWith(completeFindingStates({ 'verification-required': 2 }))], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith('Bugbot found 2 unresolved actionable finding(s).'); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: workflow.failed')); }); it('fails closed when owned finding-state evidence is malformed', async () => { @@ -589,7 +644,7 @@ describe('finishGithubAction', () => { await finishGithubAction(execution(), [malformed], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith('Bugbot finding-state evidence is malformed.'); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: provider.contract-invalid')); }); it('fails closed when review telemetry requires but omits finding-state evidence', async () => { @@ -610,7 +665,7 @@ describe('finishGithubAction', () => { await finishGithubAction(execution(), [missing], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith('Bugbot finding-state evidence is malformed.'); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: provider.contract-invalid')); }); it('fails closed when valid review state coexists with malformed telemetry', async () => { @@ -638,7 +693,7 @@ describe('finishGithubAction', () => { await finishGithubAction(execution(), [valid, malformed], {} as never, {} as never); - expect(core.setFailed).toHaveBeenCalledWith('Bugbot finding-state evidence is malformed.'); + expect(core.setFailed).toHaveBeenCalledWith(expect.stringContaining('Error code: provider.contract-invalid')); }); }); diff --git a/src/actions/__tests__/local_action.test.ts b/src/actions/__tests__/local_action.test.ts index b5183d274..d3a2bff05 100644 --- a/src/actions/__tests__/local_action.test.ts +++ b/src/actions/__tests__/local_action.test.ts @@ -34,6 +34,7 @@ jest.mock('../../data/repository/project/project_board_query_repository', () => import { runLocalAction } from '../local_action'; import { INPUT_KEYS } from '../../application/contracts/input_keys'; +import { ApplicationError } from '../../application/errors/application_error'; /** Minimal defaults so local_action can run (avoids .split on undefined). */ function minimalActionInputs(): Record { @@ -227,7 +228,7 @@ describe('runLocalAction', () => { it('includes errors and reminders in boxen content when results have errors and reminders', async () => { const boxen = require('boxen'); mockMainRun.mockResolvedValue([ - { executed: false, steps: [], errors: [new Error('Error one')], reminders: [] }, + { executed: false, steps: [], errors: [new ApplicationError('provider.unavailable', 'Provider detail.')], reminders: [] }, { executed: true, steps: [], errors: [], reminders: ['Reminder text'] }, ]); const params: Record = { @@ -240,14 +241,15 @@ describe('runLocalAction', () => { await runLocalAction(params); const content = boxen.mock.calls[0][0]; - expect(content).toContain('Error one'); + expect(content).toContain('Error code: provider.unavailable'); + expect(content).not.toContain('Provider detail.'); expect(content).toContain('Reminder text'); }); it('renders errors even when the failed operation was executed', async () => { const boxen = require('boxen'); mockMainRun.mockResolvedValue([ - { executed: true, steps: ['Attempted operation'], errors: [new Error('Executed operation failed')], reminders: [] }, + { executed: true, steps: ['Attempted operation'], errors: [new ApplicationError('workflow.failed', 'Executed operation failed.')], reminders: [] }, ]); const params: Record = { [INPUT_KEYS.TOKEN]: 't', @@ -258,7 +260,64 @@ describe('runLocalAction', () => { await runLocalAction(params); - expect(boxen.mock.calls[0][0]).toContain('Executed operation failed'); + expect(boxen.mock.calls[0][0]).toContain('Error code: workflow.failed'); + expect(boxen.mock.calls[0][0]).not.toContain('Executed operation failed.'); + }); + + it('renders one complete error view in the configured repository locale', async () => { + const boxen = require('boxen'); + mockMainRun.mockResolvedValue([{ + executed: true, + steps: [], + errors: [new ApplicationError('provider.rate-limited', 'English provider message.')], + reminders: [], + }]); + + await runLocalAction({ + [INPUT_KEYS.TOKEN]: 't', + [INPUT_KEYS.REPOSITORY_LOCALE]: 'es-MX', + repo: { owner: 'o', repo: 'r' }, + eventName: 'push', + commits: { ref: 'refs/heads/main' }, + }); + + const content = boxen.mock.calls[0][0] as string; + expect(content).toContain('Errores:'); + expect(content).toContain('Impacto: El proveedor limitó temporalmente la operación.'); + expect(content).toContain('Código de error: provider.rate-limited'); + expect(content).toContain('Acción: Reinténtalo cuando se restablezca el límite del proveedor.'); + expect(content).toContain('Reintentable: Sí'); + expect(content).not.toContain('English provider message.'); + expect(content).not.toContain('Impact:'); + }); + + it('renders validated partial-state recovery without losing its safe identifier', async () => { + const boxen = require('boxen'); + mockMainRun.mockResolvedValue([{ + executed: true, + steps: [], + errors: [new ApplicationError('provider.unavailable', 'Producer message.', { + recovery: { + id: 'inactivity-explanation-failed', + variables: { issueNumber: 42 }, + }, + })], + reminders: [], + }]); + + await runLocalAction({ + [INPUT_KEYS.TOKEN]: 't', + [INPUT_KEYS.REPOSITORY_LOCALE]: 'es-ES', + repo: { owner: 'o', repo: 'r' }, + eventName: 'push', + commits: { ref: 'refs/heads/main' }, + }); + + const content = boxen.mock.calls[0][0] as string; + expect(content).toContain('Impacto: La issue #42 se cerró sin su explicación final'); + expect(content).toContain('Estado conservado: La issue #42 permanece cerrada'); + expect(content).not.toContain('Producer message.'); + expect(content).not.toContain('El proveedor no estaba disponible'); }); it('uses custom image URLs when provided so default image arrays are not pushed', async () => { diff --git a/src/actions/github_action.ts b/src/actions/github_action.ts index 6f9f076e4..d922a9524 100644 --- a/src/actions/github_action.ts +++ b/src/actions/github_action.ts @@ -28,6 +28,10 @@ import { createLanguageQueryPort } from '../infrastructure/composition/agent_cap import { ResolveMessageCatalogUseCase } from '../application/usecases/localization/resolve_message_catalog_use_case'; import { readGithubActionLocaleInputs } from './github_action_locale_inputs'; import { publicationLocaleNeedsDynamicCatalog } from '../application/policies/publication_message_catalog'; +import { + resolveStaticApplicationErrorCatalog, + type ApplicationErrorMessageReader, +} from '../application/policies/application_error_message_catalog'; export async function runGitHubAction(): Promise { const eventInputs = buildGithubActionEventInputs({ @@ -153,12 +157,22 @@ export async function runGitHubActionEntry( } catch (cause: unknown) { const semanticError = toApplicationError(cause, 'workflow.failed', 'GitHub Action execution failed.'); logError(semanticError); - core.setFailed(renderApplicationErrorText(semanticError)); + core.setFailed(renderApplicationErrorText(semanticError, earlyGithubActionErrorMessage())); } }); } +function earlyGithubActionErrorMessage(): ApplicationErrorMessageReader { + try { + const locale = readGithubActionLocaleInputs(getGithubActionInput).repository; + return resolveStaticApplicationErrorCatalog(locale).message; + } catch { + return resolveStaticApplicationErrorCatalog('en-US').message; + } +} + // Only auto-run when executed as the action entry (not when imported by tests) +/* istanbul ignore next -- the bundled production entry is covered by the Action smoke path. */ if (typeof process.env.JEST_WORKER_ID === 'undefined') { void runGitHubActionEntry(); } diff --git a/src/actions/github_action_completion.ts b/src/actions/github_action_completion.ts index d3af83aa4..3ab8110a9 100644 --- a/src/actions/github_action_completion.ts +++ b/src/actions/github_action_completion.ts @@ -20,13 +20,14 @@ import { lifecycleStateFromLabels } from '../domain/copilot_lifecycle'; import type { CopilotEvidencePort } from '../application/ports/copilot_evidence_ports'; import { buildCopilotEvidence } from '../application/policies/copilot_evidence_policy'; import type { ActionSummaryPort } from '../application/ports/action_summary_ports'; -import { toApplicationError } from '../application/errors/application_error'; +import { ApplicationError, toApplicationError } from '../application/errors/application_error'; import { shouldPersistConfiguration } from '../application/policies/configuration_persistence_policy'; import { renderDeploymentJobSummary } from '../application/policies/deployment_presentation_policy'; import { deploymentCopy, resolveDeploymentCatalog } from '../application/policies/deployment_message_catalog'; import { projectBugbotResultFindingStates } from '../application/policies/bugbot_result_finding_state_projection_policy'; import { countActionableBugbotFindings } from '../domain/bugbot/review_state'; import type { MessageCatalogResolutionPort } from '../application/ports/message_catalog_ports'; +import type { ApplicationErrorMessageReader } from '../application/policies/application_error_message_catalog'; export async function finishGithubAction( execution: Execution, @@ -69,9 +70,15 @@ export async function finishGithubAction( logInfo('Configuration persistence skipped: this single action does not modify execution configuration.'); } const summary = await writeActionSummary(execution, summaryPort, catalogResolver); - if (!dryRun) await publishCopilotEvidence(execution, results, summary, evidencePort); - failActionForUnresolvedFindingsIfConfigured(execution, results, dryRun); - setFirstErrorIfExists(results); + if (!dryRun) await publishCopilotEvidence(execution, results, summary.text, evidencePort); + const completionError = firstApplicationError(results) + ?? bugbotCompletionError(execution, results, dryRun); + if (completionError) core.setFailed(renderApplicationErrorText(completionError, summary.errorMessage)); +} + +interface WrittenActionSummary { + readonly text: string; + readonly errorMessage: ApplicationErrorMessageReader; } function extractBugbotTelemetry(results: readonly Result[]): unknown[] { @@ -85,7 +92,7 @@ async function writeActionSummary( execution: Execution, summaryPort?: ActionSummaryPort, catalogResolver?: MessageCatalogResolutionPort, -): Promise { +): Promise { const operation = execution.currentConfiguration.deploymentOrchestration; const locale = execution.locale ?? { repository: 'en-US', issue: 'en-US', pullRequest: 'en-US' }; const summaryLocale = execution.singleAction.isDeploymentOrchestrationAction && operation?.locale @@ -94,6 +101,7 @@ async function writeActionSummary( let body: string; let localizationLabels: LocalizationSummaryLabels | undefined; let appendLocalizationEvidence = false; + let errorMessage: ApplicationErrorMessageReader; if (execution.singleAction.isDeploymentOrchestrationAction && operation) { const effectiveLocale = operation.locale ?? locale; const catalog = await resolveDeploymentCatalog( @@ -102,6 +110,7 @@ async function writeActionSummary( catalogResolver, ); const messages = deploymentCopy(catalog); + errorMessage = (id, variables) => catalog.message(id, variables); localizationLabels = { heading: messages.localization, property: messages.property, @@ -132,6 +141,7 @@ async function writeActionSummary( execution.ai.getAgentConfiguration('planner'), catalogResolver, ); + errorMessage = (id, variables) => catalog.message(id, variables); body = buildActionSummary({ owner: execution.owner, repository: execution.repo, @@ -163,13 +173,13 @@ async function writeActionSummary( }, catalogResolver?.observations?.() ?? [], localizationLabels) : ''; const summaryText = [body, localizationEvidence].filter(Boolean).join('\n\n'); - if (!summaryPort) return summaryText; + if (!summaryPort) return { text: summaryText, errorMessage }; try { await summaryPort.publish(summaryText); } catch (error) { logInfo(toApplicationError(error, 'provider.unavailable', 'Could not write the GitHub Actions summary.').message); } - return summaryText; + return { text: summaryText, errorMessage }; } async function publishCopilotEvidence( @@ -198,26 +208,27 @@ async function publishCopilotEvidence( } } -function failActionForUnresolvedFindingsIfConfigured( +function bugbotCompletionError( execution: Execution, results: readonly Result[], dryRun: boolean, -): void { - if (dryRun) return; +): ApplicationError | undefined { + if (dryRun) return undefined; const projection = projectBugbotResultFindingStates(results); if (projection.status === 'invalid') { - core.setFailed('Bugbot finding-state evidence is malformed.'); - return; + return new ApplicationError('provider.contract-invalid', 'Bugbot finding-state evidence is malformed.'); } - if (projection.status === 'absent') return; + if (projection.status === 'absent') return undefined; if (projection.counts.unknown > 0) { - core.setFailed(`Bugbot could not verify ${projection.counts.unknown} finding state(s).`); - } else if ( + return new ApplicationError('provider.contract-invalid', 'Bugbot finding-state evidence could not be verified.'); + } + if ( execution.ai.getBugbotReviewConfiguration().failOnUnresolved && countActionableBugbotFindings(projection.counts) > 0 ) { - core.setFailed(`Bugbot found ${countActionableBugbotFindings(projection.counts)} unresolved actionable finding(s).`); + return new ApplicationError('workflow.failed', 'Bugbot found unresolved actionable findings.'); } + return undefined; } function commitPublishedRecommendationState(execution: Execution, results: Result[]): void { @@ -234,11 +245,11 @@ function commitPublishedRecommendationState(execution: Execution, results: Resul } } -function setFirstErrorIfExists(results: Result[]): void { +function firstApplicationError(results: readonly Result[]): Result['errors'][number] | undefined { for (const result of results) { if (result.errors && result.errors.length > 0) { - core.setFailed(renderApplicationErrorText(result.errors[0])); - return; + return result.errors[0]; } } + return undefined; } diff --git a/src/actions/local_action_output.ts b/src/actions/local_action_output.ts index 412b8ebc8..8b0c663e7 100644 --- a/src/actions/local_action_output.ts +++ b/src/actions/local_action_output.ts @@ -44,7 +44,9 @@ export function renderLocalActionResults( const errorsContent = results .filter(result => result.errors.length > 0) - .map(result => chalk.gray(result.errors.map(renderApplicationErrorText).join('\n\n'))).join('\n'); + .map(result => chalk.gray(result.errors + .map(error => renderApplicationErrorText(error, catalog.render)) + .join('\n\n'))).join('\n'); if (errorsContent.length > 0) { content += '\n' + chalk.red(`${catalog.cli.errors}:`) + '\n' + errorsContent; diff --git a/src/actions/main_run_lifecycle.ts b/src/actions/main_run_lifecycle.ts index c1cdf5979..1fb40018e 100644 --- a/src/actions/main_run_lifecycle.ts +++ b/src/actions/main_run_lifecycle.ts @@ -1,8 +1,7 @@ -import * as core from '@actions/core'; import chalk from 'chalk'; import boxen from 'boxen'; import type { Execution } from '../data/model/execution'; -import type { Result } from '../data/model/result'; +import { Result } from '../data/model/result'; import { TITLE } from '../application/contracts/product_identity'; import { logError, logInfo } from '../utils/logger'; import { dispatchMainRunRoute } from './main_run_dispatcher'; @@ -12,7 +11,6 @@ import { resolveWorkflowIdentifier } from './workflow_context'; import { createWaitForPreviousWorkflowRunsUseCase } from '../infrastructure/composition/workflow_queue_composition_root'; import type { PreviousWorkflowRunsQuery } from '../application/ports/workflow_run_ports'; import { toApplicationError } from '../application/errors/application_error'; -import { renderApplicationErrorText } from '../application/policies/application_error_presentation_policy'; export const WORKFLOW_QUEUE_FAILURE_MESSAGE = 'Workflow queue check failed; sequential execution was not bypassed.'; @@ -117,9 +115,18 @@ export async function runMainRoute( try { let results: Result[]; if (route === 'unhandled') { - logError(`Action not handled. Event: ${execution.eventName}.`); - core.setFailed('Action not handled.'); - results = []; + const semanticError = toApplicationError( + undefined, + 'workflow.invalid-event', + 'Action not handled.', + ); + logError(semanticError); + results = [new Result({ + id: 'MainRunRoute', + success: false, + executed: false, + errors: [semanticError], + })]; } else { results = await dispatchMainRunRoute(route, execution, routeHandlers); } @@ -130,7 +137,11 @@ export async function runMainRoute( } catch (error: unknown) { const semanticError = toApplicationError(error, 'workflow.failed', 'Main run failed.'); logError(semanticError); - core.setFailed(renderApplicationErrorText(semanticError)); - return []; + return [new Result({ + id: 'MainRunRoute', + success: false, + executed: true, + errors: [semanticError], + })]; } } diff --git a/src/application/errors/__tests__/application_error.test.ts b/src/application/errors/__tests__/application_error.test.ts index ab5a1f685..13b8fb6d5 100644 --- a/src/application/errors/__tests__/application_error.test.ts +++ b/src/application/errors/__tests__/application_error.test.ts @@ -61,6 +61,37 @@ describe('ApplicationError', () => { })).toThrow('Retryability cannot be broadened'); }); + it('preserves only closed, immutable recovery descriptors with bounded variables', () => { + const error = new ApplicationError('provider.unavailable', 'Enrichment failed.', { + correlationId: CORRELATION_ID, + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: 'feature/42-add-login' }, + }, + }); + + expect(error.recovery).toEqual({ + id: 'managed-branch-enrichment-failed', + variables: { branchName: 'feature/42-add-login' }, + }); + expect(Object.isFrozen(error.recovery)).toBe(true); + expect(Object.isFrozen(error.recovery?.variables)).toBe(true); + expect(error.toJSON()).toMatchObject({ recovery: error.recovery }); + }); + + it.each([ + { id: 'managed-branch-enrichment-failed', variables: { branchName: '`forged`' } }, + { id: 'managed-branch-enrichment-failed', variables: {} }, + { id: 'inactivity-explanation-failed', variables: { issueNumber: 0 } }, + { id: 'pull-request-link-restored', variables: { unexpected: 'value' } }, + { id: 'not-supported', variables: {} }, + ])('rejects an invalid recovery descriptor: $id $variables', recovery => { + expect(() => new ApplicationError('provider.unavailable', 'Failed.', { + correlationId: CORRELATION_ID, + recovery: recovery as never, + })).toThrow('Application error recovery'); + }); + it('creates and validates lowercase UUID v4 correlation IDs', () => { expect(new ApplicationError('unexpected', 'Unexpected.').correlationId).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, diff --git a/src/application/errors/application_error.ts b/src/application/errors/application_error.ts index a47c196fb..220a134cd 100644 --- a/src/application/errors/application_error.ts +++ b/src/application/errors/application_error.ts @@ -7,9 +7,12 @@ import { createApplicationErrorCorrelationId, getApplicationErrorCorrelationId } export { APPLICATION_ERROR_METADATA, + APPLICATION_ERROR_RECOVERY_IDS, type ApplicationErrorCode, type ApplicationErrorKind, type ApplicationErrorPublicRecord, + type ApplicationErrorRecovery, + type ApplicationErrorRecoveryId, } from '../../data/model/application_error'; export interface ApplicationErrorOptions extends Omit { diff --git a/src/application/policies/__tests__/application_error_message_catalog.test.ts b/src/application/policies/__tests__/application_error_message_catalog.test.ts new file mode 100644 index 000000000..4b3d96419 --- /dev/null +++ b/src/application/policies/__tests__/application_error_message_catalog.test.ts @@ -0,0 +1,91 @@ +import { catalogPlaceholders, validateCatalogDefinition, type CatalogMessage } from '../../../domain/message_catalog'; +import { APPLICATION_ERROR_METADATA } from '../../../data/model/application_error'; +import { ResolveMessageCatalogUseCase } from '../../usecases/localization/resolve_message_catalog_use_case'; +import { + APPLICATION_ERROR_CATALOG_DEFINITIONS, + APPLICATION_ERROR_CODES, + APPLICATION_ERROR_MESSAGE_IDS, + ENGLISH_APPLICATION_ERROR_DEFINITION, + SPANISH_APPLICATION_ERROR_DEFINITION, + resolveApplicationErrorCatalog, + resolveStaticApplicationErrorCatalog, +} from '../application_error_message_catalog'; + +const configuration = { provider: 'codex' as const, model: 'model' }; + +function translatedMessages(): Record { + return Object.fromEntries(Object.entries(ENGLISH_APPLICATION_ERROR_DEFINITION.messages).map(([id, message]) => [ + id, + `FR ${message as string}`, + ])); +} + +describe('application error message catalog', () => { + it('ships complete, unique, immutable English and Spanish definitions', () => { + expect(new Set(APPLICATION_ERROR_MESSAGE_IDS).size).toBe(APPLICATION_ERROR_MESSAGE_IDS.length); + expect(APPLICATION_ERROR_CODES).toEqual(Object.keys(APPLICATION_ERROR_METADATA)); + for (const definition of APPLICATION_ERROR_CATALOG_DEFINITIONS) { + expect(validateCatalogDefinition(definition, APPLICATION_ERROR_MESSAGE_IDS)).toEqual([]); + expect(Object.isFrozen(definition)).toBe(true); + expect(Object.isFrozen(definition.messages)).toBe(true); + } + for (const id of APPLICATION_ERROR_MESSAGE_IDS) { + expect(catalogPlaceholders(SPANISH_APPLICATION_ERROR_DEFINITION.messages[id])) + .toEqual(catalogPlaceholders(ENGLISH_APPLICATION_ERROR_DEFINITION.messages[id])); + } + }); + + it.each(APPLICATION_ERROR_CODES)('derives exact English semantic content for %s', (code) => { + expect(ENGLISH_APPLICATION_ERROR_DEFINITION.messages[`error.${code}.impact`]) + .toBe(APPLICATION_ERROR_METADATA[code].impact); + expect(ENGLISH_APPLICATION_ERROR_DEFINITION.messages[`error.${code}.action`]) + .toBe(APPLICATION_ERROR_METADATA[code].action); + expect(ENGLISH_APPLICATION_ERROR_DEFINITION.messages[`error.${code}.retainedState`]) + .toBe(APPLICATION_ERROR_METADATA[code].retainedState); + }); + + it.each([ + ['en-US', 'en-US', 'exact'], + ['en-AU', 'en-US', 'base'], + ['es-ES', 'es-ES', 'exact'], + ['es-MX', 'es-ES', 'base'], + ['fr-FR', 'en-US', 'fallback'], + ] as const)('resolves %s as %s from %s', (requested, resolved, source) => { + expect(resolveStaticApplicationErrorCatalog(requested)).toMatchObject({ + requestedLocale: requested, + locale: resolved, + resolutionSource: source, + }); + }); + + it('resolves and caches a complete arbitrary-language catalog atomically', async () => { + const query = jest.fn().mockResolvedValue({ targetLocale: 'fr-FR', messages: translatedMessages() }); + const resolver = new ResolveMessageCatalogUseCase({ query }); + + const first = await resolveApplicationErrorCatalog('fr-FR', configuration, resolver); + const replay = await resolveApplicationErrorCatalog('fr-FR', configuration, resolver); + + expect(first.message('error.provider.rate-limited.action')) + .toBe('FR Retry after the provider limit resets.'); + expect(replay).toMatchObject({ locale: 'fr-FR', resolutionSource: 'dynamic' }); + expect(query).toHaveBeenCalledTimes(1); + }); + + it('falls back atomically when any translated descriptor is missing', async () => { + const messages = translatedMessages(); + delete messages['error.label.action']; + const resolver = new ResolveMessageCatalogUseCase({ + query: jest.fn().mockResolvedValue({ targetLocale: 'fr-FR', messages }), + }); + + const catalog = await resolveApplicationErrorCatalog('fr-FR', configuration, resolver); + + expect(catalog).toMatchObject({ + locale: 'en-US', + resolutionSource: 'fallback', + fallbackReason: 'dynamic-response-invalid', + }); + expect(catalog.message('error.label.action')).toBe('Action'); + expect(catalog.message('error.provider.unavailable.impact')).not.toContain('FR '); + }); +}); diff --git a/src/application/policies/__tests__/application_error_presentation_policy.test.ts b/src/application/policies/__tests__/application_error_presentation_policy.test.ts index a006351cd..c8e547a2d 100644 --- a/src/application/policies/__tests__/application_error_presentation_policy.test.ts +++ b/src/application/policies/__tests__/application_error_presentation_policy.test.ts @@ -1,11 +1,58 @@ -import { APPLICATION_ERROR_METADATA, ApplicationError, type ApplicationErrorCode } from '../../errors/application_error'; +import { + APPLICATION_ERROR_METADATA, + APPLICATION_ERROR_RECOVERY_IDS, + ApplicationError, + type ApplicationErrorCode, + type ApplicationErrorRecovery, +} from '../../errors/application_error'; import { buildApplicationErrorPresentation, renderApplicationErrorText, } from '../application_error_presentation_policy'; +import { resolveStaticApplicationErrorCatalog } from '../application_error_message_catalog'; const CORRELATION_ID = '6f173f89-96a3-4fc8-b90e-4f8f48e0e319'; +const RECOVERY_CASES: readonly Readonly<{ + recovery: ApplicationErrorRecovery; + english: string; + spanish: string; +}>[] = [ + { + recovery: { id: 'pull-request-link-restored', variables: {} }, + english: 'The original pull-request base and description were restored.', + spanish: 'Se restauraron la rama base y la descripción originales de la pull request.', + }, + { + recovery: { id: 'pull-request-link-base-retained', variables: {} }, + english: 'The temporary default base branch remains; the original description was restored.', + spanish: 'La rama base predeterminada temporal permanece; se restauró la descripción original.', + }, + { + recovery: { id: 'pull-request-link-reference-retained', variables: {} }, + english: 'The original base branch was restored; the temporary issue reference remains in the description.', + spanish: 'Se restauró la rama base original; la referencia temporal a la issue permanece en la descripción.', + }, + { + recovery: { id: 'pull-request-link-base-and-reference-retained', variables: {} }, + english: 'The temporary default base branch and issue reference remain.', + spanish: 'La rama base predeterminada temporal y la referencia a la issue permanecen.', + }, + { + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: 'feature/42-localized-errors' }, + }, + english: 'Branch feature/42-localized-errors and its configuration patch were preserved.', + spanish: 'Se conservaron la rama feature/42-localized-errors y su parche de configuración.', + }, + { + recovery: { id: 'inactivity-explanation-failed', variables: { issueNumber: 42 } }, + english: 'Issue #42 remains closed; the completed close will not be repeated.', + spanish: 'La issue #42 permanece cerrada; el cierre completado no se repetirá.', + }, +]; + describe('application error presentation policy', () => { it.each(Object.keys(APPLICATION_ERROR_METADATA) as ApplicationErrorCode[])( 'renders the complete stable view for %s', @@ -16,19 +63,61 @@ describe('application error presentation policy', () => { expect(view).toEqual({ impact: error.impact, - cause: 'Safe cause.', code, action: error.action, retainedState: error.retainedState, + retryable: error.retryable ? 'Yes' : 'No', reference: CORRELATION_ID, }); expect(text).toBe([ `Impact: ${error.impact}`, - `Cause (${code}): Safe cause.`, + `Error code: ${code}`, `Action: ${error.action}`, `Retained state: ${error.retainedState}`, + `Retryable: ${error.retryable ? 'Yes' : 'No'}`, `Reference: ${CORRELATION_ID}`, ].join('\n')); + expect(text).not.toContain('Safe cause.'); }, ); + + it.each(Object.keys(APPLICATION_ERROR_METADATA) as ApplicationErrorCode[])( + 'renders every %s recovery view atomically in Spanish', + (code) => { + const error = new ApplicationError(code, 'English producer message.', { correlationId: CORRELATION_ID }); + const catalog = resolveStaticApplicationErrorCatalog('es-MX'); + const text = renderApplicationErrorText(error, catalog.message); + + expect(text).toContain(`Código de error: ${code}`); + expect(text).toContain('Impacto:'); + expect(text).toContain('Acción:'); + expect(text).toContain('Estado conservado:'); + expect(text).toContain('Reintentable:'); + expect(text).toContain(`Referencia: ${CORRELATION_ID}`); + expect(text).not.toContain('English producer message.'); + expect(text).not.toContain('Impact:'); + expect(catalog.resolutionSource).toBe('base'); + }, + ); + + it.each(RECOVERY_CASES)( + 'preserves and localizes recovery variant $recovery.id', + ({ recovery, english, spanish }) => { + const error = new ApplicationError('provider.unavailable', 'Producer message.', { + correlationId: CORRELATION_ID, + recovery, + }); + + expect(buildApplicationErrorPresentation(error).retainedState).toBe(english); + expect(buildApplicationErrorPresentation( + error, + resolveStaticApplicationErrorCatalog('es-ES').message, + ).retainedState).toBe(spanish); + }, + ); + + it('covers every closed recovery variant exactly once', () => { + expect(RECOVERY_CASES.map(({ recovery }) => recovery.id).sort()) + .toEqual([...APPLICATION_ERROR_RECOVERY_IDS].sort()); + }); }); diff --git a/src/application/policies/__tests__/inactivity_message_catalog.test.ts b/src/application/policies/__tests__/inactivity_message_catalog.test.ts index 26bbaaa23..d78c2c9d8 100644 --- a/src/application/policies/__tests__/inactivity_message_catalog.test.ts +++ b/src/application/policies/__tests__/inactivity_message_catalog.test.ts @@ -33,9 +33,6 @@ const germanMessages = Object.freeze({ 'inactivity.error.revalidate': 'Issue #{issueNumber} konnte vor dem Schließen nicht erneut geprüft werden.', 'inactivity.error.close': 'Issue #{issueNumber} konnte nicht wegen Inaktivität geschlossen werden.', 'inactivity.error.comment': 'Issue #{issueNumber} wurde geschlossen, aber die Erklärung konnte nicht veröffentlicht werden.', - 'inactivity.error.commentImpact': 'Issue #{issueNumber} wurde ohne abschließende Erklärung geschlossen.', - 'inactivity.error.commentAction': 'Prüfen Sie Issue #{issueNumber} und ergänzen Sie die Erklärung bei Bedarf manuell.', - 'inactivity.error.commentRetainedState': 'Issue #{issueNumber} bleibt geschlossen; der Abschluss wird nicht wiederholt.', }); describe('inactivity message catalog', () => { diff --git a/src/application/policies/action_summary_message_catalog.ts b/src/application/policies/action_summary_message_catalog.ts index 552aef54a..36ec8a0a4 100644 --- a/src/application/policies/action_summary_message_catalog.ts +++ b/src/application/policies/action_summary_message_catalog.ts @@ -1,5 +1,4 @@ import type { AgentConfiguration } from '../../domain/agent'; -import type { ApplicationErrorKind } from '../../data/model/application_error'; import { MESSAGE_CATALOG_VERSION, type CatalogMessage, @@ -11,6 +10,12 @@ import { resolveStaticMessageCatalogView, type ResolvedMessageCatalogView, } from './resolved_message_catalog_policy'; +import { + APPLICATION_ERROR_MESSAGE_IDS, + ENGLISH_APPLICATION_ERROR_MESSAGES, + SPANISH_APPLICATION_ERROR_MESSAGES, + type ApplicationErrorMessageId, +} from './application_error_message_catalog'; const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'heading', 'repository', 'property', 'value', 'status', 'event', 'target', @@ -18,9 +23,7 @@ const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'resultDetails', 'localization', 'repositoryLocale', 'issueLocale', 'pullRequestLocale', 'catalogResolution', 'descriptors', 'reason', 'failure', 'findings', 'partial', 'superseded', 'skipped', 'dryRun', 'success', 'invalid', - 'none', 'impact', 'cause', 'action', - 'retainedState', 'reference', 'retryable', 'yes', 'no', - 'resultSucceeded', 'resultFailed', 'resultSkipped', + 'none', 'resultSucceeded', 'resultFailed', 'resultSkipped', ] as const); const TEMPLATE_MESSAGE_IDS = Object.freeze([ @@ -35,28 +38,24 @@ const FINDING_STATE_KEYS = Object.freeze([ 'verification-required', 'unknown', ] as const); -const ERROR_KIND_KEYS: readonly ApplicationErrorKind[] = Object.freeze([ - 'configuration', 'authorization', 'provider', 'agent', 'validation', 'workflow', 'unknown', -]); - -const ERROR_FIELD_KEYS = Object.freeze(['impact', 'action', 'retainedState'] as const); - type SimpleMessageKey = typeof SIMPLE_MESSAGE_KEYS[number]; type SimpleMessageId = `summary.${SimpleMessageKey}`; type TemplateMessageId = typeof TEMPLATE_MESSAGE_IDS[number]; export type ActionSummaryFindingState = typeof FINDING_STATE_KEYS[number]; type FindingStateMessageId = `summary.findingState.${ActionSummaryFindingState}`; -type ErrorField = typeof ERROR_FIELD_KEYS[number]; -type ErrorMessageId = `summary.error.${ApplicationErrorKind}.${ErrorField}`; -export type ActionSummaryMessageId = SimpleMessageId | TemplateMessageId | FindingStateMessageId | ErrorMessageId; +export type ActionSummaryMessageId = + | SimpleMessageId + | TemplateMessageId + | FindingStateMessageId + | ApplicationErrorMessageId; export type ActionSummaryMessageCatalog = ResolvedMessageCatalogView; export const ACTION_SUMMARY_MESSAGE_IDS: readonly ActionSummaryMessageId[] = Object.freeze([ ...SIMPLE_MESSAGE_KEYS.map(key => `summary.${key}` as const), ...TEMPLATE_MESSAGE_IDS, ...FINDING_STATE_KEYS.map(key => `summary.findingState.${key}` as const), - ...ERROR_KIND_KEYS.flatMap(kind => ERROR_FIELD_KEYS.map(field => `summary.error.${kind}.${field}` as const)), + ...APPLICATION_ERROR_MESSAGE_IDS, ]); const ENGLISH_SIMPLE: Readonly> = Object.freeze({ @@ -89,14 +88,6 @@ const ENGLISH_SIMPLE: Readonly> = Object.freeze success: 'Success', invalid: 'invalid', none: 'none', - impact: 'Impact', - cause: 'Error code', - action: 'Action', - retainedState: 'Retained state', - reference: 'Reference', - retryable: 'Retryable', - yes: 'Yes', - no: 'No', resultSucceeded: 'Succeeded', resultFailed: 'Failed', resultSkipped: 'Skipped', @@ -132,14 +123,6 @@ const SPANISH_SIMPLE: Readonly> = Object.freeze success: 'Correcto', invalid: 'no válido', none: 'ninguno', - impact: 'Impacto', - cause: 'Código de error', - action: 'Acción', - retainedState: 'Estado conservado', - reference: 'Referencia', - retryable: 'Reintentable', - yes: 'Sí', - no: 'No', resultSucceeded: 'Completado', resultFailed: 'Fallido', resultSkipped: 'Omitido', @@ -179,98 +162,17 @@ const SPANISH_FINDING_STATES: Readonly unknown: 'desconocidos', }); -type ErrorMessages = Readonly>>>; - -const ENGLISH_ERRORS: ErrorMessages = Object.freeze({ - configuration: Object.freeze({ - impact: 'The operation could not use the configured values.', - action: 'Correct the configuration or choose a supported capability before retrying.', - retainedState: 'No new state or external effect was created.', - }), - authorization: Object.freeze({ - impact: 'The operation could not authenticate or access a required resource.', - action: 'Correct the credential or grant the documented permission before retrying.', - retainedState: 'No new state or external effect was created.', - }), - provider: Object.freeze({ - impact: 'A provider operation did not complete.', - action: 'Inspect the error code and retry only when the provider state or availability has changed.', - retainedState: 'Existing state and completed external effects remain in place.', - }), - agent: Object.freeze({ - impact: 'The configured agent did not produce usable product content.', - action: 'Inspect the sanitized agent status and retry with a compatible provider or model.', - retainedState: 'Existing state remains in place; rejected content was not published.', - }), - validation: Object.freeze({ - impact: 'The supplied input was rejected before the operation could continue.', - action: 'Correct the input and retry.', - retainedState: 'No new state or external effect was created.', - }), - workflow: Object.freeze({ - impact: 'The workflow could not complete the requested operation.', - action: 'Inspect the current state and retry only if the operation is still required.', - retainedState: 'Existing state and confirmed completed effects remain in place.', - }), - unknown: Object.freeze({ - impact: 'An unexpected failure was handled safely.', - action: 'Use the reference to investigate before retrying.', - retainedState: 'Existing state and confirmed completed effects remain in place.', - }), -}); - -const SPANISH_ERRORS: ErrorMessages = Object.freeze({ - configuration: Object.freeze({ - impact: 'La operación no pudo usar los valores configurados.', - action: 'Corrige la configuración o elige una capacidad compatible antes de reintentarlo.', - retainedState: 'No se creó ningún estado ni efecto externo nuevo.', - }), - authorization: Object.freeze({ - impact: 'La operación no pudo autenticarse o acceder a un recurso necesario.', - action: 'Corrige la credencial o concede el permiso documentado antes de reintentarlo.', - retainedState: 'No se creó ningún estado ni efecto externo nuevo.', - }), - provider: Object.freeze({ - impact: 'Una operación del proveedor no se completó.', - action: 'Revisa el código de error y reinténtalo solo cuando haya cambiado el estado o la disponibilidad del proveedor.', - retainedState: 'El estado existente y los efectos externos completados se mantienen.', - }), - agent: Object.freeze({ - impact: 'El agente configurado no produjo contenido de producto utilizable.', - action: 'Revisa el estado saneado del agente y reinténtalo con un proveedor o modelo compatible.', - retainedState: 'El estado existente se mantiene y el contenido rechazado no se publicó.', - }), - validation: Object.freeze({ - impact: 'La entrada suministrada se rechazó antes de continuar la operación.', - action: 'Corrige la entrada y reinténtalo.', - retainedState: 'No se creó ningún estado ni efecto externo nuevo.', - }), - workflow: Object.freeze({ - impact: 'El workflow no pudo completar la operación solicitada.', - action: 'Revisa el estado actual y reinténtalo solo si la operación sigue siendo necesaria.', - retainedState: 'El estado existente y los efectos completados y confirmados se mantienen.', - }), - unknown: Object.freeze({ - impact: 'Un fallo inesperado se gestionó de forma segura.', - action: 'Usa la referencia para investigar antes de reintentarlo.', - retainedState: 'El estado existente y los efectos completados y confirmados se mantienen.', - }), -}); - function catalogMessages( simple: Readonly>, templates: Readonly>, findingStates: Readonly>, - errors: ErrorMessages, + errorMessages: Readonly>, ): Readonly> { return Object.freeze({ ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`summary.${key}`, simple[key]])), ...templates, ...Object.fromEntries(FINDING_STATE_KEYS.map(key => [`summary.findingState.${key}`, findingStates[key]])), - ...Object.fromEntries(ERROR_KIND_KEYS.flatMap(kind => ERROR_FIELD_KEYS.map(field => [ - `summary.error.${kind}.${field}`, - errors[kind][field], - ]))), + ...errorMessages, }) as Readonly>; } @@ -278,14 +180,24 @@ export const ENGLISH_ACTION_SUMMARY_DEFINITION: MessageCatalogDefinition = Object.freeze({ version: MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', - messages: catalogMessages(SPANISH_SIMPLE, SPANISH_TEMPLATES, SPANISH_FINDING_STATES, SPANISH_ERRORS), + messages: catalogMessages( + SPANISH_SIMPLE, + SPANISH_TEMPLATES, + SPANISH_FINDING_STATES, + SPANISH_APPLICATION_ERROR_MESSAGES, + ), }); export const ACTION_SUMMARY_CATALOG_DEFINITIONS = Object.freeze([ diff --git a/src/application/policies/action_summary_policy.ts b/src/application/policies/action_summary_policy.ts index 1a8716ca6..eb78788dd 100644 --- a/src/application/policies/action_summary_policy.ts +++ b/src/application/policies/action_summary_policy.ts @@ -16,6 +16,8 @@ import { type ActionSummaryFindingState, type ActionSummaryMessageCatalog, } from './action_summary_message_catalog'; +import { buildApplicationErrorPresentation } from './application_error_presentation_policy'; +import type { ApplicationErrorMessageReader } from './application_error_message_catalog'; export interface ActionSummaryContext { readonly owner: string; @@ -247,16 +249,18 @@ function resultFailed(result: Result): boolean { } function renderFailures(results: readonly Result[], catalog: ActionSummaryMessageCatalog): string { + const message: ApplicationErrorMessageReader = (id, variables) => catalogText(catalog, id, variables); return results.map(result => { const errors = result.errors .flatMap((error) => { + const view = buildApplicationErrorPresentation(error, message); return [ - ` - **${catalogText(catalog, 'summary.impact')}:** ${errorCatalogText(catalog, error.kind, 'impact')}`, - ` - **${catalogText(catalog, 'summary.cause')}:** \`${error.code}\``, - ` - **${catalogText(catalog, 'summary.action')}:** ${errorCatalogText(catalog, error.kind, 'action')}`, - ` - **${catalogText(catalog, 'summary.retainedState')}:** ${errorCatalogText(catalog, error.kind, 'retainedState')}`, - ` - **${catalogText(catalog, 'summary.retryable')}:** ${catalogText(catalog, error.retryable ? 'summary.yes' : 'summary.no')}`, - ` - **${catalogText(catalog, 'summary.reference')}:** \`${error.correlationId}\``, + ` - **${message('error.label.impact')}:** ${view.impact}`, + ` - **${message('error.label.errorCode')}:** \`${view.code}\``, + ` - **${message('error.label.action')}:** ${view.action}`, + ` - **${message('error.label.retainedState')}:** ${view.retainedState}`, + ` - **${message('error.label.retryable')}:** ${view.retryable}`, + ` - **${message('error.label.reference')}:** \`${view.reference}\``, ]; }); return [ @@ -266,14 +270,6 @@ function renderFailures(results: readonly Result[], catalog: ActionSummaryMessag }).join('\n'); } -function errorCatalogText( - catalog: ActionSummaryMessageCatalog, - kind: Result['errors'][number]['kind'], - field: 'impact' | 'action' | 'retainedState', -): string { - return catalogText(catalog, `summary.error.${kind}.${field}`); -} - function catalogText( catalog: ActionSummaryMessageCatalog, id: Parameters[0], diff --git a/src/application/policies/application_error_message_catalog.ts b/src/application/policies/application_error_message_catalog.ts new file mode 100644 index 000000000..c475880f2 --- /dev/null +++ b/src/application/policies/application_error_message_catalog.ts @@ -0,0 +1,331 @@ +import type { + ApplicationErrorCode, + ApplicationErrorRecoveryId, +} from '../../data/model/application_error'; +import { + APPLICATION_ERROR_METADATA, + APPLICATION_ERROR_RECOVERY_IDS, +} from '../../data/model/application_error'; +import { + MESSAGE_CATALOG_VERSION, + renderCatalogMessage, + type CatalogMessage, + type MessageCatalogDefinition, +} from '../../domain/message_catalog'; +import type { AgentConfiguration } from '../../domain/agent'; +import type { MessageCatalogResolutionPort } from '../ports/message_catalog_ports'; +import { + resolveMessageCatalogView, + resolveStaticMessageCatalogView, + type ResolvedMessageCatalogView, +} from './resolved_message_catalog_policy'; + +const LABEL_KEYS = Object.freeze([ + 'impact', 'errorCode', 'action', 'retainedState', 'retryable', 'yes', 'no', 'reference', +] as const); +const CONTENT_FIELDS = Object.freeze(['impact', 'action', 'retainedState'] as const); + +export const APPLICATION_ERROR_CODES = Object.freeze( + Object.keys(APPLICATION_ERROR_METADATA) as ApplicationErrorCode[], +); + +type LabelKey = typeof LABEL_KEYS[number]; +type ContentField = typeof CONTENT_FIELDS[number]; +type LabelMessageId = `error.label.${LabelKey}`; +type ContentMessageId = `error.${ApplicationErrorCode}.${ContentField}`; +type RecoveryMessageId = `error.recovery.${ApplicationErrorRecoveryId}.${ContentField}`; + +export type ApplicationErrorMessageId = LabelMessageId | ContentMessageId | RecoveryMessageId; +export type ApplicationErrorMessageReader = ( + id: ApplicationErrorMessageId, + variables?: Readonly>, +) => string; +export type ApplicationErrorMessageCatalog = ResolvedMessageCatalogView; + +export const APPLICATION_ERROR_MESSAGE_IDS: readonly ApplicationErrorMessageId[] = Object.freeze([ + ...LABEL_KEYS.map(key => `error.label.${key}` as const), + ...APPLICATION_ERROR_CODES.flatMap(code => CONTENT_FIELDS.map(field => `error.${code}.${field}` as const)), + ...APPLICATION_ERROR_RECOVERY_IDS.flatMap(id => CONTENT_FIELDS + .map(field => `error.recovery.${id}.${field}` as const)), +]); + +const ENGLISH_LABELS: Readonly> = Object.freeze({ + impact: 'Impact', + errorCode: 'Error code', + action: 'Action', + retainedState: 'Retained state', + retryable: 'Retryable', + yes: 'Yes', + no: 'No', + reference: 'Reference', +}); + +const SPANISH_LABELS: Readonly> = Object.freeze({ + impact: 'Impacto', + errorCode: 'Código de error', + action: 'Acción', + retainedState: 'Estado conservado', + retryable: 'Reintentable', + yes: 'Sí', + no: 'No', + reference: 'Referencia', +}); + +type ErrorContent = Readonly>>>; +type RecoveryContent = Readonly< + Record>> +>; + +const ENGLISH_CONTENT: ErrorContent = Object.freeze(Object.fromEntries( + APPLICATION_ERROR_CODES.map(code => [code, Object.freeze({ + impact: APPLICATION_ERROR_METADATA[code].impact, + action: APPLICATION_ERROR_METADATA[code].action, + retainedState: APPLICATION_ERROR_METADATA[code].retainedState, + })]), +) as Record>>); + +const UNCHANGED_STATE_ES = 'No se creó ningún estado ni efecto externo nuevo.'; +const PRESERVED_STATE_ES = 'Se conservaron el estado persistente existente y los efectos externos completados.'; + +const SPANISH_CONTENT: ErrorContent = Object.freeze({ + 'configuration.invalid': Object.freeze({ + impact: 'La operación no pudo usar los valores configurados.', + action: 'Corrige la configuración no válida y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'configuration.unsupported': Object.freeze({ + impact: 'La capacidad solicitada no es compatible con esta instalación.', + action: 'Usa una configuración compatible o actualiza la instalación.', + retainedState: UNCHANGED_STATE_ES, + }), + 'authorization.denied': Object.freeze({ + impact: 'La operación no pudo acceder al recurso necesario.', + action: 'Concede el permiso documentado y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'authorization.credential-invalid': Object.freeze({ + impact: 'La operación no pudo autenticarse con el proveedor necesario.', + action: 'Sustituye o configura la credencial necesaria y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'provider.not-found': Object.freeze({ + impact: 'No se encontró un recurso necesario del proveedor.', + action: 'Verifica el recurso de destino y reintenta la operación.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.conflict': Object.freeze({ + impact: 'El proveedor rechazó un estado actual conflictivo.', + action: 'Vuelve a cargar el estado actual y reinténtalo si la operación sigue siendo necesaria.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.rate-limited': Object.freeze({ + impact: 'El proveedor limitó temporalmente la operación.', + action: 'Reinténtalo cuando se restablezca el límite del proveedor.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.unavailable': Object.freeze({ + impact: 'El proveedor no estaba disponible temporalmente.', + action: 'Reinténtalo cuando el proveedor esté disponible.', + retainedState: PRESERVED_STATE_ES, + }), + 'provider.contract-invalid': Object.freeze({ + impact: 'La respuesta del proveedor no se pudo interpretar de forma segura.', + action: 'Revisa la integración del proveedor antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), + 'agent.policy-rejected': Object.freeze({ + impact: 'El agente configurado no se inició.', + action: 'Usa una configuración de agente permitida y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'agent.failed': Object.freeze({ + impact: 'El agente admitido no produjo un resultado utilizable.', + action: 'Revisa el estado saneado del agente y reinténtalo si procede.', + retainedState: PRESERVED_STATE_ES, + }), + 'locale.output-invalid': Object.freeze({ + impact: 'El contenido de producto generado por el agente se rechazó antes de publicarse porque su contrato de locale no era válido.', + action: 'Reinténtalo con un proveedor compatible con el locale configurado del repositorio.', + retainedState: UNCHANGED_STATE_ES, + }), + 'locale.translation-failed': Object.freeze({ + impact: 'La solicitud no se pudo interpretar de forma segura en el idioma configurado del repositorio.', + action: 'Reformula la solicitud o reinténtalo cuando esté disponible el proveedor de idioma configurado.', + retainedState: UNCHANGED_STATE_ES, + }), + 'validation.invalid-input': Object.freeze({ + impact: 'La operación no aceptó la entrada suministrada.', + action: 'Corrige la entrada y reinténtalo.', + retainedState: UNCHANGED_STATE_ES, + }), + 'workflow.invalid-event': Object.freeze({ + impact: 'El evento no puede iniciar el workflow solicitado.', + action: 'Inicia la operación desde un evento o superficie compatible.', + retainedState: UNCHANGED_STATE_ES, + }), + 'workflow.stale': Object.freeze({ + impact: 'Un estado más reciente sustituyó esta ejecución del workflow.', + action: 'Revisa el estado actual e inicia una nueva ejecución solo si es necesario.', + retainedState: PRESERVED_STATE_ES, + }), + 'workflow.cancelled': Object.freeze({ + impact: 'El workflow se detuvo antes de completarse.', + action: 'Inicia una nueva ejecución si la operación sigue siendo necesaria.', + retainedState: PRESERVED_STATE_ES, + }), + 'workflow.failed': Object.freeze({ + impact: 'El workflow no pudo completar la operación solicitada.', + action: 'Revisa el estado actual y reintenta el paso fallido.', + retainedState: PRESERVED_STATE_ES, + }), + timeout: Object.freeze({ + impact: 'La operación superó su tiempo de ejecución limitado.', + action: 'Verifica el estado actual antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), + unexpected: Object.freeze({ + impact: 'La operación se detuvo porque un fallo inesperado se gestionó de forma segura.', + action: 'Usa el ID de correlación para investigar antes de reintentarlo.', + retainedState: PRESERVED_STATE_ES, + }), +}); + +const ENGLISH_RECOVERY_CONTENT: RecoveryContent = Object.freeze({ + 'pull-request-link-restored': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Rerun the workflow; no temporary pull-request state needs manual recovery.', + retainedState: 'The original pull-request base and description were restored.', + }), + 'pull-request-link-base-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Restore the temporary default base branch, then rerun the workflow.', + retainedState: 'The temporary default base branch remains; the original description was restored.', + }), + 'pull-request-link-reference-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Remove the temporary issue reference from the description, then rerun the workflow.', + retainedState: 'The original base branch was restored; the temporary issue reference remains in the description.', + }), + 'pull-request-link-base-and-reference-retained': Object.freeze({ + impact: 'The pull request could not be linked to its issue.', + action: 'Restore the temporary default base branch and remove the temporary issue reference, then rerun the workflow.', + retainedState: 'The temporary default base branch and issue reference remain.', + }), + 'managed-branch-enrichment-failed': Object.freeze({ + impact: 'The linked branch exists, but later issue metadata may be incomplete.', + action: 'Continue on {branchName} and rerun issue enrichment.', + retainedState: 'Branch {branchName} and its configuration patch were preserved.', + }), + 'inactivity-explanation-failed': Object.freeze({ + impact: 'Issue #{issueNumber} was closed without its terminal inactivity explanation.', + action: 'Inspect issue #{issueNumber} and add the explanation manually if the missing context matters.', + retainedState: 'Issue #{issueNumber} remains closed; the completed close will not be repeated.', + }), +}); + +const SPANISH_RECOVERY_CONTENT: RecoveryContent = Object.freeze({ + 'pull-request-link-restored': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Vuelve a ejecutar el workflow; ningún estado temporal de la pull request requiere recuperación manual.', + retainedState: 'Se restauraron la rama base y la descripción originales de la pull request.', + }), + 'pull-request-link-base-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Restaura la rama base predeterminada temporal y vuelve a ejecutar el workflow.', + retainedState: 'La rama base predeterminada temporal permanece; se restauró la descripción original.', + }), + 'pull-request-link-reference-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Elimina la referencia temporal a la issue de la descripción y vuelve a ejecutar el workflow.', + retainedState: 'Se restauró la rama base original; la referencia temporal a la issue permanece en la descripción.', + }), + 'pull-request-link-base-and-reference-retained': Object.freeze({ + impact: 'No se pudo vincular la pull request con su issue.', + action: 'Restaura la rama base predeterminada temporal, elimina la referencia temporal a la issue y vuelve a ejecutar el workflow.', + retainedState: 'La rama base predeterminada temporal y la referencia a la issue permanecen.', + }), + 'managed-branch-enrichment-failed': Object.freeze({ + impact: 'La rama vinculada existe, pero los metadatos posteriores de la issue pueden estar incompletos.', + action: 'Continúa en {branchName} y vuelve a ejecutar el enriquecimiento de la issue.', + retainedState: 'Se conservaron la rama {branchName} y su parche de configuración.', + }), + 'inactivity-explanation-failed': Object.freeze({ + impact: 'La issue #{issueNumber} se cerró sin su explicación final sobre la inactividad.', + action: 'Revisa la issue #{issueNumber} y añade la explicación manualmente si falta contexto importante.', + retainedState: 'La issue #{issueNumber} permanece cerrada; el cierre completado no se repetirá.', + }), +}); + +function catalogMessages( + labels: Readonly>, + content: ErrorContent, + recoveryContent: RecoveryContent, +): Readonly> { + return Object.freeze({ + ...Object.fromEntries(LABEL_KEYS.map(key => [`error.label.${key}`, labels[key]])), + ...Object.fromEntries(APPLICATION_ERROR_CODES.flatMap(code => CONTENT_FIELDS.map(field => [ + `error.${code}.${field}`, + content[code][field], + ]))), + ...Object.fromEntries(APPLICATION_ERROR_RECOVERY_IDS.flatMap(id => CONTENT_FIELDS.map(field => [ + `error.recovery.${id}.${field}`, + recoveryContent[id][field], + ]))), + }) as Readonly>; +} + +export const ENGLISH_APPLICATION_ERROR_MESSAGES = catalogMessages( + ENGLISH_LABELS, + ENGLISH_CONTENT, + ENGLISH_RECOVERY_CONTENT, +); +export const SPANISH_APPLICATION_ERROR_MESSAGES = catalogMessages( + SPANISH_LABELS, + SPANISH_CONTENT, + SPANISH_RECOVERY_CONTENT, +); + +export const ENGLISH_APPLICATION_ERROR_DEFINITION: MessageCatalogDefinition = Object.freeze({ + version: MESSAGE_CATALOG_VERSION, + locale: 'en-US', + compatibleBaseLanguage: 'en', + messages: ENGLISH_APPLICATION_ERROR_MESSAGES, +}); + +export const SPANISH_APPLICATION_ERROR_DEFINITION: MessageCatalogDefinition = Object.freeze({ + version: MESSAGE_CATALOG_VERSION, + locale: 'es-ES', + compatibleBaseLanguage: 'es', + messages: SPANISH_APPLICATION_ERROR_MESSAGES, +}); + +export const APPLICATION_ERROR_CATALOG_DEFINITIONS = Object.freeze([ + ENGLISH_APPLICATION_ERROR_DEFINITION, + SPANISH_APPLICATION_ERROR_DEFINITION, +]); + +export function resolveStaticApplicationErrorCatalog(locale: string): ApplicationErrorMessageCatalog { + return resolveStaticMessageCatalogView( + locale, + ENGLISH_APPLICATION_ERROR_DEFINITION, + APPLICATION_ERROR_CATALOG_DEFINITIONS, + ); +} + +export function resolveApplicationErrorCatalog( + locale: string, + configuration: Readonly | undefined, + resolver: MessageCatalogResolutionPort | undefined, +): Promise { + return resolveMessageCatalogView( + locale, + APPLICATION_ERROR_MESSAGE_IDS, + ENGLISH_APPLICATION_ERROR_DEFINITION, + APPLICATION_ERROR_CATALOG_DEFINITIONS, + configuration, + resolver, + ); +} + +export const readEnglishApplicationErrorMessage: ApplicationErrorMessageReader = (id, variables = {}) => + renderCatalogMessage(ENGLISH_APPLICATION_ERROR_MESSAGES[id], variables, 'en-US'); diff --git a/src/application/policies/application_error_presentation_policy.ts b/src/application/policies/application_error_presentation_policy.ts index a3092a0c8..d86178b59 100644 --- a/src/application/policies/application_error_presentation_policy.ts +++ b/src/application/policies/application_error_presentation_policy.ts @@ -1,33 +1,48 @@ import type { ApplicationError } from '../../data/model/application_error'; +import { + readEnglishApplicationErrorMessage, + type ApplicationErrorMessageReader, +} from './application_error_message_catalog'; export interface ApplicationErrorPresentation { readonly impact: string; - readonly cause: string; readonly code: string; readonly action: string; readonly retainedState: string; + readonly retryable: string; readonly reference: string; } /** Shared semantic view model for terminal, GitHub, and API presentation. */ -export function buildApplicationErrorPresentation(error: ApplicationError): ApplicationErrorPresentation { +export function buildApplicationErrorPresentation( + error: ApplicationError, + message: ApplicationErrorMessageReader = readEnglishApplicationErrorMessage, +): ApplicationErrorPresentation { + const descriptor = error.recovery + ? `error.recovery.${error.recovery.id}` as const + : `error.${error.code}` as const; + const variables = error.recovery?.variables; return { - impact: error.impact, - cause: error.message, + impact: message(`${descriptor}.impact`, variables), code: error.code, - action: error.action, - retainedState: error.retainedState, + action: message(`${descriptor}.action`, variables), + retainedState: message(`${descriptor}.retainedState`, variables), + retryable: message(error.retryable ? 'error.label.yes' : 'error.label.no'), reference: error.correlationId, }; } -export function renderApplicationErrorText(error: ApplicationError): string { - const view = buildApplicationErrorPresentation(error); +export function renderApplicationErrorText( + error: ApplicationError, + message: ApplicationErrorMessageReader = readEnglishApplicationErrorMessage, +): string { + const view = buildApplicationErrorPresentation(error, message); return [ - `Impact: ${view.impact}`, - `Cause (${view.code}): ${view.cause}`, - `Action: ${view.action}`, - `Retained state: ${view.retainedState}`, - `Reference: ${view.reference}`, + `${message('error.label.impact')}: ${view.impact}`, + `${message('error.label.errorCode')}: ${view.code}`, + `${message('error.label.action')}: ${view.action}`, + `${message('error.label.retainedState')}: ${view.retainedState}`, + `${message('error.label.retryable')}: ${view.retryable}`, + `${message('error.label.reference')}: ${view.reference}`, ].join('\n'); } diff --git a/src/application/policies/deployment_message_catalog.ts b/src/application/policies/deployment_message_catalog.ts index 5c7b02425..e9a326a08 100644 --- a/src/application/policies/deployment_message_catalog.ts +++ b/src/application/policies/deployment_message_catalog.ts @@ -20,6 +20,12 @@ import { SPANISH_MERGE_QUEUE_MESSAGES, type MergeQueueMessageId, } from './merge_queue_message_catalog'; +import { + APPLICATION_ERROR_MESSAGE_IDS, + ENGLISH_APPLICATION_ERROR_MESSAGES, + SPANISH_APPLICATION_ERROR_MESSAGES, + type ApplicationErrorMessageId, +} from './application_error_message_catalog'; const SIMPLE_MESSAGE_KEYS = Object.freeze([ 'release', 'hotfix', 'currentStatus', 'noAction', 'actionRequired', 'progress', @@ -82,7 +88,8 @@ export type DeploymentMessageId = | PhaseMessageId | DiagramMessageId | TemplateMessageId - | MergeQueueMessageId; + | MergeQueueMessageId + | ApplicationErrorMessageId; export type DeploymentMessageCatalog = ResolvedMessageCatalogView; export interface DeploymentCopy extends Readonly> { @@ -96,6 +103,7 @@ export const DEPLOYMENT_MESSAGE_IDS: readonly DeploymentMessageId[] = Object.fre ...DIAGRAM_KEYS.map(key => `deployment.diagram.${key}` as const), ...TEMPLATE_MESSAGE_IDS, ...MERGE_QUEUE_MESSAGE_IDS, + ...APPLICATION_ERROR_MESSAGE_IDS, ]); const ENGLISH_SIMPLE: Readonly> = Object.freeze({ @@ -244,6 +252,7 @@ function catalogMessages( diagram: Readonly>, templates: Readonly>, mergeQueue: Readonly>, + errorMessages: Readonly>, ): Readonly> { return Object.freeze({ ...Object.fromEntries(SIMPLE_MESSAGE_KEYS.map(key => [`deployment.${key}`, simple[key]])), @@ -251,6 +260,7 @@ function catalogMessages( ...Object.fromEntries(DIAGRAM_KEYS.map(key => [`deployment.diagram.${key}`, diagram[key]])), ...templates, ...mergeQueue, + ...errorMessages, }) as Readonly>; } @@ -264,6 +274,7 @@ export const ENGLISH_DEPLOYMENT_DEFINITION: MessageCatalogDefinition> = 'inactivity.error.revalidate': 'Unable to recheck issue #{issueNumber} before inactivity closure.', 'inactivity.error.close': 'Unable to close issue #{issueNumber} after inactivity.', 'inactivity.error.comment': 'Issue #{issueNumber} was closed, but its inactivity explanation could not be published.', - 'inactivity.error.commentImpact': 'Issue #{issueNumber} was closed without its terminal inactivity explanation.', - 'inactivity.error.commentAction': 'Inspect issue #{issueNumber} and add the explanation manually if the missing context matters.', - 'inactivity.error.commentRetainedState': 'Issue #{issueNumber} remains closed; the completed close will not be repeated.', }); const SPANISH_MESSAGES: Readonly> = Object.freeze({ @@ -88,9 +82,6 @@ const SPANISH_MESSAGES: Readonly> = 'inactivity.error.revalidate': 'No se pudo volver a comprobar la issue #{issueNumber} antes de cerrarla por inactividad.', 'inactivity.error.close': 'No se pudo cerrar la issue #{issueNumber} por inactividad.', 'inactivity.error.comment': 'La issue #{issueNumber} se cerró, pero no se pudo publicar la explicación sobre su inactividad.', - 'inactivity.error.commentImpact': 'La issue #{issueNumber} se cerró sin su explicación final sobre la inactividad.', - 'inactivity.error.commentAction': 'Revisa la issue #{issueNumber} y añade la explicación manualmente si falta contexto importante.', - 'inactivity.error.commentRetainedState': 'La issue #{issueNumber} permanece cerrada; el cierre completado no se repetirá.', }); export const ENGLISH_INACTIVITY_DEFINITION: MessageCatalogDefinition = Object.freeze({ diff --git a/src/application/policies/publication_message_catalog.ts b/src/application/policies/publication_message_catalog.ts index 45cee3f5d..9f9a6225f 100644 --- a/src/application/policies/publication_message_catalog.ts +++ b/src/application/policies/publication_message_catalog.ts @@ -2,14 +2,21 @@ import { MESSAGE_CATALOG_VERSION, renderCatalogMessage, selectBundledMessageCatalog, + type CatalogMessage, type MessageCatalogDefinition, type ResolvedMessageCatalog, } from '../../domain/message_catalog'; import { canonicalizeLocaleTag, DEFAULT_REPOSITORY_LOCALE } from '../../domain/locale'; import type { AgentConfiguration } from '../../domain/agent'; import type { MessageCatalogResolutionPort } from '../ports/message_catalog_ports'; +import { + APPLICATION_ERROR_MESSAGE_IDS, + ENGLISH_APPLICATION_ERROR_MESSAGES, + SPANISH_APPLICATION_ERROR_MESSAGES, + type ApplicationErrorMessageId, +} from './application_error_message_catalog'; -export const PUBLICATION_MESSAGE_IDS = Object.freeze([ +const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'publication.implementationPlan', 'publication.planReady', 'publication.planAcceptance', @@ -81,7 +88,13 @@ export const PUBLICATION_MESSAGE_IDS = Object.freeze([ 'cli.reminder', ] as const); -export type PublicationMessageId = typeof PUBLICATION_MESSAGE_IDS[number]; +type PublicationSurfaceMessageId = typeof PUBLICATION_SURFACE_MESSAGE_IDS[number]; +export type PublicationMessageId = PublicationSurfaceMessageId | ApplicationErrorMessageId; + +export const PUBLICATION_MESSAGE_IDS: readonly PublicationMessageId[] = Object.freeze([ + ...PUBLICATION_SURFACE_MESSAGE_IDS, + ...APPLICATION_ERROR_MESSAGE_IDS, +]); export interface PublicationMessageCatalog { readonly locale: string; @@ -116,7 +129,7 @@ export interface PublicationMessageCatalog { readonly render: (id: PublicationMessageId, variables?: Readonly>) => string; } -const ENGLISH_MESSAGES: Readonly> = Object.freeze({ +const ENGLISH_MESSAGES: Readonly> = Object.freeze({ 'publication.implementationPlan': 'Implementation plan', 'publication.planReady': 'Ready to start. No action is required from maintainers before implementation.', 'publication.planAcceptance': 'Acceptance', @@ -186,9 +199,10 @@ const ENGLISH_MESSAGES: Readonly> = Object. 'cli.steps': 'Steps', 'cli.errors': 'Errors', 'cli.reminder': 'Reminder', + ...ENGLISH_APPLICATION_ERROR_MESSAGES, }); -const SPANISH_MESSAGES: Readonly> = Object.freeze({ +const SPANISH_MESSAGES: Readonly> = Object.freeze({ 'publication.implementationPlan': 'Plan de implementación', 'publication.planReady': 'Listo para comenzar. No se requiere ninguna acción de mantenimiento antes de la implementación.', 'publication.planAcceptance': 'Aceptación', @@ -258,6 +272,7 @@ const SPANISH_MESSAGES: Readonly> = Object. 'cli.steps': 'Pasos', 'cli.errors': 'Errores', 'cli.reminder': 'Recordatorio', + ...SPANISH_APPLICATION_ERROR_MESSAGES, }); export const ENGLISH_PUBLICATION_DEFINITION: MessageCatalogDefinition = Object.freeze({ diff --git a/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts b/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts index d7eb8f606..ab6cb255e 100644 --- a/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts +++ b/src/application/usecases/actions/__tests__/close_inactive_issues_use_case.test.ts @@ -136,9 +136,6 @@ describe('CloseInactiveIssuesUseCase', () => { 'inactivity.error.revalidate': 'Issue #{issueNumber} konnte nicht erneut geprüft werden.', 'inactivity.error.close': 'Issue #{issueNumber} konnte nicht geschlossen werden.', 'inactivity.error.comment': 'Issue #{issueNumber} wurde geschlossen, aber die Erklärung fehlt.', - 'inactivity.error.commentImpact': 'Issue #{issueNumber} wurde ohne Erklärung geschlossen.', - 'inactivity.error.commentAction': 'Issue #{issueNumber} manuell prüfen.', - 'inactivity.error.commentRetainedState': 'Issue #{issueNumber} bleibt geschlossen.', }; const query = jest.fn().mockResolvedValue({ targetLocale: 'de-DE', messages }); @@ -231,9 +228,10 @@ describe('CloseInactiveIssuesUseCase', () => { ); expect(result.errors[0]).toMatchObject({ retryable: false, - impact: 'Issue #42 was closed without its terminal inactivity explanation.', - action: 'Inspect issue #42 and add the explanation manually if the missing context matters.', - retainedState: 'Issue #42 remains closed; the completed close will not be repeated.', + recovery: { + id: 'inactivity-explanation-failed', + variables: { issueNumber: 42 }, + }, }); expect(result.payload).toMatchObject({ closed: 1, commented: 0, commentFailures: 1, failures: 1, diff --git a/src/application/usecases/actions/close_inactive_issues_workflow.ts b/src/application/usecases/actions/close_inactive_issues_workflow.ts index 16316536a..f61336640 100644 --- a/src/application/usecases/actions/close_inactive_issues_workflow.ts +++ b/src/application/usecases/actions/close_inactive_issues_workflow.ts @@ -134,16 +134,10 @@ export async function runCloseInactiveIssuesWorkflow( errors.push(new ApplicationError('provider.unavailable', message, { cause: error, retryable: false, - impact: resultMessages.message('inactivity.error.commentImpact', { - issueNumber: candidate.number, - }), - action: resultMessages.message('inactivity.error.commentAction', { - issueNumber: candidate.number, - }), - retainedState: resultMessages.message( - 'inactivity.error.commentRetainedState', - { issueNumber: candidate.number }, - ), + recovery: { + id: 'inactivity-explanation-failed', + variables: { issueNumber: candidate.number }, + }, })); } } diff --git a/src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts b/src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts index 2237d6037..c6f22b54c 100644 --- a/src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts +++ b/src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts @@ -541,7 +541,10 @@ describe("PrepareBranchesUseCase", () => { executed: true, errors: [expect.objectContaining({ code: 'provider.unavailable', - retainedState: 'The branch feature/42-add-login-feature and its configuration patch were preserved.', + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: 'feature/42-add-login-feature' }, + }, })], }); expect(mockMoveIssueInvoke).not.toHaveBeenCalled(); diff --git a/src/application/usecases/steps/issue/prepare_managed_branch.ts b/src/application/usecases/steps/issue/prepare_managed_branch.ts index b11da6748..54aec80cb 100644 --- a/src/application/usecases/steps/issue/prepare_managed_branch.ts +++ b/src/application/usecases/steps/issue/prepare_managed_branch.ts @@ -109,9 +109,10 @@ export async function prepareManagedBranch( 'provider.unavailable', 'The branch was created, but its linked issue state could not be synchronized.', { - impact: 'The linked branch exists, but later issue metadata may be incomplete.', - action: 'Continue on the retained branch and rerun issue enrichment.', - retainedState: `The branch ${branchPayload.newBranchName} and its configuration patch were preserved.`, + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: branchPayload.newBranchName }, + }, }, ); result.push(new Result({ diff --git a/src/application/usecases/steps/pull_request/__tests__/link_pull_request_issue_use_case.test.ts b/src/application/usecases/steps/pull_request/__tests__/link_pull_request_issue_use_case.test.ts index 71a3f42e1..f7ffef4ff 100644 --- a/src/application/usecases/steps/pull_request/__tests__/link_pull_request_issue_use_case.test.ts +++ b/src/application/usecases/steps/pull_request/__tests__/link_pull_request_issue_use_case.test.ts @@ -108,7 +108,10 @@ describe('LinkPullRequestIssueUseCase', () => { expect(results.at(-1)).toMatchObject({ success: false, errors: [expect.objectContaining({ - retainedState: 'The original pull-request base and description were restored.', + recovery: { + id: 'pull-request-link-restored', + variables: {}, + }, })], }); }); @@ -173,6 +176,10 @@ describe('LinkPullRequestIssueUseCase', () => { const results = await useCase.invoke(context()); expect(results.at(-1)?.success).toBe(false); expect(results.at(-1)?.steps[0]).toContain('retained the temporary default base branch'); + expect(results.at(-1)?.errors[0].recovery).toEqual({ + id: 'pull-request-link-base-retained', + variables: {}, + }); expect(mockUpdateDescription).toHaveBeenCalledTimes(2); }); @@ -184,9 +191,29 @@ describe('LinkPullRequestIssueUseCase', () => { expect(results.at(-1)?.steps).toEqual([ 'Pull-request issue linkage failed and retained the temporary issue reference in the description. Restore that PR state, then re-run the workflow.', ]); + expect(results.at(-1)?.errors[0].recovery).toEqual({ + id: 'pull-request-link-reference-retained', + variables: {}, + }); expect(mockUpdateBaseBranch).toHaveBeenLastCalledWith(10, 'develop'); }); + it('reports both retained temporary states when neither restoration succeeds', async () => { + mockUpdateBaseBranch + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('base restore failed')); + mockUpdateDescription + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('description restore failed')); + + const results = await useCase.invoke(context()); + + expect(results.at(-1)?.errors[0].recovery).toEqual({ + id: 'pull-request-link-base-and-reference-retained', + variables: {}, + }); + }); + it('resumes an exact pending marker without layering another reference', async () => { const pending = ''; mockGetDetails.mockResolvedValue({ body: `Current PR body\n\nResolves #42\n\n${pending}`, baseBranch: 'main' }); diff --git a/src/application/usecases/steps/pull_request/link_pull_request_issue_use_case.ts b/src/application/usecases/steps/pull_request/link_pull_request_issue_use_case.ts index 4c42322e0..4a7ac9288 100644 --- a/src/application/usecases/steps/pull_request/link_pull_request_issue_use_case.ts +++ b/src/application/usecases/steps/pull_request/link_pull_request_issue_use_case.ts @@ -9,7 +9,10 @@ import { PullRequestIssueLinkOperationError, runLinkPullRequestIssue, } from './link_pull_request_issue_workflow'; -import { toApplicationError } from '../../../errors/application_error'; +import { + toApplicationError, + type ApplicationErrorOptions, +} from '../../../errors/application_error'; export class LinkPullRequestIssueUseCase implements ParamUseCase { taskId = 'LinkPullRequestIssueUseCase'; @@ -35,8 +38,7 @@ export class LinkPullRequestIssueUseCase implements ParamUseCase { + const id = error.retainedBaseBranch + ? error.retainedIssueReference + ? 'pull-request-link-base-and-reference-retained' + : 'pull-request-link-base-retained' + : error.retainedIssueReference + ? 'pull-request-link-reference-retained' + : 'pull-request-link-restored'; + return { id, variables: {} }; } function describeRecovery(error: PullRequestIssueLinkOperationError): string { diff --git a/src/architecture/__tests__/github_publication_boundaries.test.ts b/src/architecture/__tests__/github_publication_boundaries.test.ts index 33e075f87..00bdfb6e3 100644 --- a/src/architecture/__tests__/github_publication_boundaries.test.ts +++ b/src/architecture/__tests__/github_publication_boundaries.test.ts @@ -124,7 +124,23 @@ describe('GitHub conversation publication boundaries', () => { expect(presentation).not.toMatch(/result\.steps/u); expect(presentation).not.toMatch(/result\.id/u); - expect(presentation).not.toContain('buildApplicationErrorPresentation'); + expect(presentation).toContain('buildApplicationErrorPresentation'); + }); + + it('keeps semantic failure presentation at one locale-aware completion boundary', () => { + const lifecycle = readFileSync(join(root, 'src/actions/main_run_lifecycle.ts'), 'utf8'); + const completion = readFileSync(join(root, 'src/actions/github_action_completion.ts'), 'utf8'); + const localOutput = readFileSync(join(root, 'src/actions/local_action_output.ts'), 'utf8'); + const presentation = readFileSync( + join(root, 'src/application/policies/application_error_presentation_policy.ts'), + 'utf8', + ); + + expect(lifecycle).not.toContain('core.setFailed'); + expect(completion.match(/core\.setFailed/gu)).toHaveLength(1); + expect(completion).toContain('core.setFailed(renderApplicationErrorText(completionError, summary.errorMessage))'); + expect(localOutput).toContain('renderApplicationErrorText(error, catalog.render)'); + expect(presentation).not.toMatch(/error\.message/u); }); it('keeps Bugbot public presentation free of pseudo-plural copy', () => { diff --git a/src/data/model/application_error.ts b/src/data/model/application_error.ts index 030785ae4..2c06a9560 100644 --- a/src/data/model/application_error.ts +++ b/src/data/model/application_error.ts @@ -29,6 +29,33 @@ export type ApplicationErrorCode = | 'timeout' | 'unexpected'; +export const APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ + 'pull-request-link-restored', + 'pull-request-link-base-retained', + 'pull-request-link-reference-retained', + 'pull-request-link-base-and-reference-retained', + 'managed-branch-enrichment-failed', + 'inactivity-explanation-failed', +] as const); + +export type ApplicationErrorRecoveryId = typeof APPLICATION_ERROR_RECOVERY_IDS[number]; + +interface ApplicationErrorRecoveryVariables { + readonly 'pull-request-link-restored': Readonly>; + readonly 'pull-request-link-base-retained': Readonly>; + readonly 'pull-request-link-reference-retained': Readonly>; + readonly 'pull-request-link-base-and-reference-retained': Readonly>; + readonly 'managed-branch-enrichment-failed': Readonly<{ branchName: string }>; + readonly 'inactivity-explanation-failed': Readonly<{ issueNumber: number }>; +} + +export type ApplicationErrorRecovery = { + readonly [Id in ApplicationErrorRecoveryId]: Readonly<{ + id: Id; + variables: ApplicationErrorRecoveryVariables[Id]; + }>; +}[ApplicationErrorRecoveryId]; + interface ApplicationErrorMetadata { readonly kind: ApplicationErrorKind; readonly retryable: boolean; @@ -165,9 +192,7 @@ export const APPLICATION_ERROR_METADATA: Readonly> = Object.freeze({ + 'pull-request-link-restored': Object.freeze([]), + 'pull-request-link-base-retained': Object.freeze([]), + 'pull-request-link-reference-retained': Object.freeze([]), + 'pull-request-link-base-and-reference-retained': Object.freeze([]), + 'managed-branch-enrichment-failed': Object.freeze(['branchName']), + 'inactivity-explanation-failed': Object.freeze(['issueNumber']), +}); + +function normalizeApplicationErrorRecovery( + recovery: ApplicationErrorRecovery | undefined, +): ApplicationErrorRecovery | undefined { + if (!recovery) return undefined; + if (!APPLICATION_ERROR_RECOVERY_IDS.includes(recovery.id)) { + throw new TypeError('Application error recovery ID is invalid.'); + } + const variables = recovery.variables as Readonly>; + const actualKeys = Object.keys(variables).sort(); + const expectedKeys = [...RECOVERY_VARIABLE_KEYS[recovery.id]].sort(); + if (actualKeys.length !== expectedKeys.length + || actualKeys.some((key, index) => key !== expectedKeys[index])) { + throw new TypeError(`Application error recovery variables are invalid for ${recovery.id}.`); + } + if (recovery.id === 'managed-branch-enrichment-failed' + && (typeof variables.branchName !== 'string' + || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/u.test(variables.branchName))) { + throw new TypeError('Application error recovery branch name is invalid.'); + } + if (recovery.id === 'inactivity-explanation-failed' + && (typeof variables.issueNumber !== 'number' + || !Number.isSafeInteger(variables.issueNumber) + || variables.issueNumber < 1)) { + throw new TypeError('Application error recovery issue number is invalid.'); + } + return Object.freeze({ + id: recovery.id, + variables: Object.freeze({ ...variables }), + }) as ApplicationErrorRecovery; +} diff --git a/src/domain/message_catalog.ts b/src/domain/message_catalog.ts index 35c3231d1..f29f37c66 100644 --- a/src/domain/message_catalog.ts +++ b/src/domain/message_catalog.ts @@ -1,6 +1,6 @@ import { baseLanguage, canonicalizeLocaleTag, DEFAULT_REPOSITORY_LOCALE } from './locale'; -export const MESSAGE_CATALOG_VERSION = '1'; +export const MESSAGE_CATALOG_VERSION = '2'; export type CatalogResolutionSource = 'exact' | 'base' | 'dynamic' | 'fallback'; export type CatalogFallbackReason = diff --git a/src/utils/__tests__/logger.test.ts b/src/utils/__tests__/logger.test.ts index 5c47287c9..d98ea192e 100644 --- a/src/utils/__tests__/logger.test.ts +++ b/src/utils/__tests__/logger.test.ts @@ -119,12 +119,18 @@ describe('logger', () => { const err = new ApplicationError('provider.unavailable', 'e', { correlationId: CORRELATION_ID, cause: new Error('raw-provider-secret'), + recovery: { + id: 'managed-branch-enrichment-failed', + variables: { branchName: 'feature/42-safe' }, + }, }); logError(err); const call = consoleErrorSpy.mock.calls[0][0] as string; expect(call).toMatch(/"level":"error"/); expect(call).toMatch(/"message":"e"/); expect(call).toContain(`"correlationId":"${CORRELATION_ID}"`); + expect(call).toContain('"recovery":{"id":"managed-branch-enrichment-failed"'); + expect(call).toContain('"branchName":"feature/42-safe"'); expect(call).not.toMatch(/stack|raw-provider-secret/); }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 707179fe1..18fcf992b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -171,6 +171,7 @@ export function logError(message: string | ApplicationErrorPublicRecord, metadat action: message.action, retainedState: message.retainedState, correlationId: message.correlationId, + ...(message.recovery ? { recovery: message.recovery } : {}), }, }); emitLog(