diff --git a/build/cli/index.js b/build/cli/index.js index a41af0ae4..4073c1018 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -38228,7 +38228,7 @@ exports.shouldTrackAgentActivity = shouldTrackAgentActivity; const agent_1 = __nccwpck_require__(89040); const copilot_command_1 = __nccwpck_require__(11771); const branch_sync_command_1 = __nccwpck_require__(51114); -const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'doctor', 'history', 'dismiss', 'remember']); +const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'sdd-diff', 'doctor', 'history', 'dismiss', 'remember']); /** Decides whether a route can invoke an agent for its current event. */ function shouldTrackAgentActivity(execution, route) { if (!hasTarget(execution)) @@ -42064,6 +42064,7 @@ const READ_ONLY_COMMANDS = Object.freeze([ ['/copilot recheck', 'interaction.help.command.recheck'], ['/copilot description', 'interaction.help.command.description'], ['/copilot status', 'interaction.help.command.status'], + ['/copilot sdd-diff', 'interaction.help.command.sddDiff'], ['/copilot doctor', 'interaction.help.command.doctor'], ['/copilot history', 'interaction.help.command.history'], ]); @@ -44364,6 +44365,7 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.help.command.recheck', 'interaction.help.command.description', 'interaction.help.command.status', + 'interaction.help.command.sddDiff', 'interaction.help.command.doctor', 'interaction.help.command.history', 'interaction.history.fixPushed', @@ -44567,6 +44569,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 're-run the review and reconcile findings.', 'interaction.help.command.description': 'refresh the pull-request description.', 'interaction.help.command.status': 'show the current automation status.', + 'interaction.help.command.sddDiff': 'compare verified first and current SDD publications without a model.', 'interaction.help.command.doctor': 'check available runtime prerequisites without making repository changes.', 'interaction.help.command.history': 'show up to ten supported operations and their verified receipts.', 'interaction.history.fixPushed': 'Fix changes were pushed. Findings require an independent review of the pushed revision.', @@ -44779,6 +44782,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 'repite la revisión y reconcilia los hallazgos.', 'interaction.help.command.description': 'actualiza la descripción del pull request.', 'interaction.help.command.status': 'muestra el estado actual de la automatización.', + 'interaction.help.command.sddDiff': 'compara las publicaciones inicial y actual verificadas del SDD sin modelo.', 'interaction.help.command.doctor': 'comprueba los requisitos disponibles del runtime sin modificar el repositorio.', 'interaction.help.command.history': 'muestra hasta diez operaciones compatibles y sus recibos verificables.', 'interaction.history.fixPushed': 'Se publicaron los cambios de la corrección. Los hallazgos requieren una revisión independiente de esa revisión.', @@ -46051,6 +46055,198 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 2230: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPANISH_SDD_DIFF_DEFINITION = exports.ENGLISH_SDD_DIFF_DEFINITION = exports.SDD_DIFF_MESSAGE_IDS = void 0; +exports.resolveSddDiffCatalog = resolveSddDiffCatalog; +exports.isSddDiffResolvedLocale = isSddDiffResolvedLocale; +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +exports.SDD_DIFF_MESSAGE_IDS = Object.freeze([ + 'sddDiff.heading', 'sddDiff.status', 'sddDiff.action', 'sddDiff.not-applicable', 'sddDiff.no-publication', + 'sddDiff.initial-only', 'sddDiff.stale', 'sddDiff.pending-revision', 'sddDiff.invalid', 'sddDiff.unavailable', + 'sddDiff.ambiguous', 'sddDiff.bounded', 'sddDiff.compared', 'sddDiff.none', 'sddDiff.publish', 'sddDiff.refresh', + 'sddDiff.answer', 'sddDiff.retry', 'sddDiff.choose', 'sddDiff.reduce', 'sddDiff.first', 'sddDiff.current', + 'sddDiff.historical', 'sddDiff.historicalCurrent', 'sddDiff.card', 'sddDiff.commit', 'sddDiff.compare', 'sddDiff.lines', 'sddDiff.sections', 'sddDiff.omitted', 'sddDiff.structural', + 'sddDiff.charactersLimit', 'sddDiff.bytesLimit', 'sddDiff.linesLimit', 'sddDiff.workLimit', +]); +const EN = Object.freeze({ + 'sddDiff.heading': 'SDD structural comparison', 'sddDiff.status': 'Current status', 'sddDiff.action': 'Next action', + 'sddDiff.not-applicable': 'An SDD comparison is not applicable to this target.', + 'sddDiff.no-publication': 'No verified SDD publication is available yet.', + 'sddDiff.initial-only': 'Only the first publication is verified; there is no revision pair to compare.', + 'sddDiff.stale': 'Current issue, SDD or branch evidence changed or disagrees. Historical evidence does not certify a current comparison.', + 'sddDiff.pending-revision': 'The requested SDD revision is pending. Current change counts are unavailable.', + 'sddDiff.invalid': 'The owning SDD evidence could not be verified.', + 'sddDiff.unavailable': 'The comparison could not be verified. No current change counts are available.', + 'sddDiff.ambiguous': 'More than one SDD-eligible issue is linked to this PR.', + 'sddDiff.bounded': 'The verified documents exceed a fixed comparison limit; exact counts are unavailable.', + 'sddDiff.compared': 'Compared verified published revisions.', 'sddDiff.none': 'No action is required.', + 'sddDiff.publish': 'Publish the SDD through the existing issue workflow, then retry this command.', + 'sddDiff.refresh': 'Retry this command with current evidence; if the issue changed, resume its existing SDD clarification workflow.', + 'sddDiff.answer': 'Answer the existing SDD questions or resume publication, then retry this command.', + 'sddDiff.retry': 'Inspect the existing SDD evidence and read access, then retry this command.', + 'sddDiff.choose': 'Run this command in the intended issue conversation.', + 'sddDiff.reduce': 'Keep the SDD within the documented fixed limits, then retry this command.', + 'sddDiff.first': 'First publication', 'sddDiff.current': 'Current publication', 'sddDiff.historical': 'Historical first publication', 'sddDiff.historicalCurrent': 'Historical published revision', + 'sddDiff.card': 'SDD clarification card', 'sddDiff.commit': 'Publication commit', 'sddDiff.compare': 'Compare publication commits', + 'sddDiff.lines': 'Added lines: {added}; removed lines: {removed}.', 'sddDiff.sections': 'Changed section titles', + 'sddDiff.omitted': 'Additional changed titles omitted: {count}.', + 'sddDiff.charactersLimit': 'Limit: 70,000 characters per document.', 'sddDiff.bytesLimit': 'Limit: 280,000 UTF-8 bytes per document.', + 'sddDiff.linesLimit': 'Limit: 2,000 content lines per document.', 'sddDiff.workLimit': 'Limit: 2,000,000 line-comparison steps.', + 'sddDiff.structural': 'This compares structure and content lines; it does not assess semantic requirements.', +}); +const ES = Object.freeze({ + 'sddDiff.heading': 'Comparación estructural del SDD', 'sddDiff.status': 'Estado actual', 'sddDiff.action': 'Siguiente acción', + 'sddDiff.not-applicable': 'La comparación de SDD no corresponde a este destino.', + 'sddDiff.no-publication': 'Todavía no hay una publicación de SDD verificada.', + 'sddDiff.initial-only': 'Sólo está verificada la primera publicación; no hay dos revisiones que comparar.', + 'sddDiff.stale': 'La evidencia de la issue, del SDD o de la rama cambió o no coincide. La evidencia histórica no certifica una comparación actual.', + 'sddDiff.pending-revision': 'La revisión solicitada del SDD está pendiente. No hay recuentos de cambios actuales.', + 'sddDiff.invalid': 'No se pudo verificar la evidencia del SDD propietario.', + 'sddDiff.unavailable': 'No se pudo verificar la comparación. No hay recuentos de cambios actuales.', + 'sddDiff.ambiguous': 'Esta PR enlaza más de una issue apta para comparar SDD.', + 'sddDiff.bounded': 'Los documentos verificados superan un límite fijo de comparación; no hay recuentos exactos.', + 'sddDiff.compared': 'Se compararon las revisiones publicadas y verificadas.', 'sddDiff.none': 'No se requiere ninguna acción.', + 'sddDiff.publish': 'Publica el SDD mediante el flujo existente de la issue y vuelve a ejecutar el comando.', + 'sddDiff.refresh': 'Vuelve a ejecutar el comando con evidencia actual; si cambió la issue, retoma su flujo existente de aclaraciones del SDD.', + 'sddDiff.answer': 'Responde las preguntas existentes del SDD o retoma su publicación y vuelve a ejecutar el comando.', + 'sddDiff.retry': 'Revisa la evidencia existente del SDD y los permisos de lectura y vuelve a ejecutar el comando.', + 'sddDiff.choose': 'Ejecuta el comando en la conversación de la issue deseada.', + 'sddDiff.reduce': 'Mantén el SDD dentro de los límites fijos documentados y vuelve a ejecutar el comando.', + 'sddDiff.first': 'Primera publicación', 'sddDiff.current': 'Publicación actual', 'sddDiff.historical': 'Primera publicación histórica', 'sddDiff.historicalCurrent': 'Revisión publicada histórica', + 'sddDiff.card': 'Tarjeta de aclaraciones del SDD', 'sddDiff.commit': 'Commit de publicación', 'sddDiff.compare': 'Comparar commits de publicación', + 'sddDiff.lines': 'Líneas añadidas: {added}; líneas eliminadas: {removed}.', 'sddDiff.sections': 'Títulos de secciones modificadas', + 'sddDiff.omitted': 'Títulos modificados adicionales omitidos: {count}.', + 'sddDiff.charactersLimit': 'Límite: 70.000 caracteres por documento.', 'sddDiff.bytesLimit': 'Límite: 280.000 bytes UTF-8 por documento.', + 'sddDiff.linesLimit': 'Límite: 2.000 líneas de contenido por documento.', 'sddDiff.workLimit': 'Límite: 2.000.000 pasos de comparación de líneas.', + 'sddDiff.structural': 'La comparación incluye estructura y líneas de contenido; no evalúa requisitos semánticos.', +}); +exports.ENGLISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', messages: EN, +}); +exports.SPANISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', messages: ES, +}); +function resolveSddDiffCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_SDD_DIFF_DEFINITION, [exports.ENGLISH_SDD_DIFF_DEFINITION, exports.SPANISH_SDD_DIFF_DEFINITION]); +} +/** Locale admission belongs to the complete bundled catalog boundary. */ +function isSddDiffResolvedLocale(value) { + return value === 'en-US' || value === 'es-ES'; +} + + +/***/ }), + +/***/ 63302: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.renderSddDiff = renderSddDiff; +const pre_branch_sdd_1 = __nccwpck_require__(34730); +const STATES = ['not-applicable', 'no-publication', 'initial-only', 'stale', 'pending-revision', 'invalid', 'unavailable', 'ambiguous', 'bounded', 'compared']; +const ACTIONS = Object.freeze({ + 'not-applicable': 'sddDiff.none', 'no-publication': 'sddDiff.publish', 'initial-only': 'sddDiff.none', + stale: 'sddDiff.refresh', 'pending-revision': 'sddDiff.answer', invalid: 'sddDiff.retry', unavailable: 'sddDiff.retry', + ambiguous: 'sddDiff.choose', bounded: 'sddDiff.reduce', compared: 'sddDiff.none', +}); +/** Immutable links are constructed only from a bound server/repository and verified publication facts. */ +function renderSddDiff(snapshot, scope, catalog) { + const render = catalog.message; + const heading = `## ${render('sddDiff.heading')}`; + const fallback = `${heading}\n\n${render('sddDiff.unavailable')}\n\n**${render('sddDiff.action')}:** ${render('sddDiff.retry')}`; + const root = repositoryRoot(scope); + if (!root || !STATES.includes(snapshot.state)) + return fallback; + const lines = [heading, '', `**${render('sddDiff.status')}:** ${render(`sddDiff.${snapshot.state}`)}`, '', + `**${render('sddDiff.action')}:** ${render(ACTIONS[snapshot.state])}`]; + if ('publication' in snapshot && snapshot.publication) { + const publication = snapshot.publication; + if (!(0, pre_branch_sdd_1.isSafeSddPath)(publication.path) || !sha(publication.firstSha)) + return fallback; + const historical = snapshot.state === 'stale' || snapshot.state === 'pending-revision'; + if (historical) { + lines.push('', link('sddDiff.historical', publication.firstSha, publication.path)); + if (publication.currentSha !== undefined) { + if (!sha(publication.currentSha)) + return fallback; + if (publication.currentSha !== publication.firstSha) + lines.push(link('sddDiff.historicalCurrent', publication.currentSha, publication.path)); + } + } + else { + lines.push('', link('sddDiff.first', publication.firstSha, publication.path)); + if (snapshot.state !== 'initial-only') { + if (!sha(publication.currentSha) || publication.currentSha === publication.firstSha) + return fallback; + lines.push(link('sddDiff.current', publication.currentSha, publication.path)); + } + } + } + else if (snapshot.state === 'initial-only' || snapshot.state === 'compared' || snapshot.state === 'bounded') + return fallback; + if (snapshot.card && (snapshot.state === 'stale' || snapshot.state === 'pending-revision')) { + if (!positive(snapshot.card.issueNumber) || !positive(snapshot.card.commentId)) + return fallback; + lines.push('', `[${render('sddDiff.card')}](${root}/issues/${snapshot.card.issueNumber}#issuecomment-${snapshot.card.commentId})`); + } + if (snapshot.state === 'compared' || snapshot.state === 'bounded') { + const publication = snapshot.publication; + lines.push('', `[${render('sddDiff.compare')}](${root}/compare/${publication.firstSha}...${publication.currentSha})`); + } + if (snapshot.state === 'bounded') { + if (!['characters', 'bytes', 'lines', 'work'].includes(snapshot.reason)) + return fallback; + lines.push('', render(`sddDiff.${snapshot.reason}Limit`)); + } + if (snapshot.state === 'compared') { + const changes = snapshot.changes; + if (changes.state !== 'compared' || !count(changes.addedLines, 2000) || !count(changes.removedLines, 2000) + || !count(changes.omittedSections, 4000) || !Array.isArray(changes.sections) || changes.sections.length > 8 + || changes.sections.some(title => typeof title !== 'string' || title.length > 120)) + return fallback; + lines.push('', render('sddDiff.lines', { added: changes.addedLines, removed: changes.removedLines }), '', render('sddDiff.structural')); + if (changes.sections.length) + lines.push('', `**${render('sddDiff.sections')}:**`, ...changes.sections.map(title => `- ${escapeTitle(title)}`)); + if (changes.omittedSections) + lines.push('', render('sddDiff.omitted', { count: changes.omittedSections })); + } + const body = lines.join('\n'); + return body.length <= 5600 ? body : fallback; + function link(label, commit, path) { + return `[${render(label)} · ${commit.slice(0, 9)}](${root}/blob/${commit}/${path}) · [${render('sddDiff.commit')}](${root}/commit/${commit})`; + } +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function count(value, max) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= max; } +function sha(value) { return typeof value === 'string' && /^[a-f0-9]{40}$/u.test(value); } +function repositoryRoot(scope) { + if (scope.serverOrigin.length > 200 || ![scope.owner, scope.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return undefined; + try { + const url = new URL(scope.serverOrigin); + return url.protocol === 'https:' && !url.username && !url.password && url.pathname === '/' && !url.search && !url.hash + ? `${url.origin}/${scope.owner}/${scope.repository}` : undefined; + } + catch { + return undefined; + } +} +function escapeTitle(value) { + return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').replace(/@/gu, '@\u200b') + .replace(/([\\`*_{}[\]()<>#|~!])/gu, '\\$1'); +} + + /***/ }), /***/ 99167: @@ -46260,6 +46456,7 @@ exports.hasOwnedPrimaryIssuePublication = hasOwnedPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const github_runtime_doctor_presentation_policy_1 = __nccwpck_require__(2808); const copilot_operation_1 = __nccwpck_require__(40221); const copilot_history_presentation_policy_1 = __nccwpck_require__(55507); @@ -46362,6 +46559,11 @@ function selectSemanticReplyIntents(context) { return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; const publication = (0, result_1.getResultPayload)(payload.publication); const kind = publication?.kind; + if (kind === 'sdd-diff' && result.id.endsWith('.SddDiff') + && typeof publication?.markdown === 'string' && publication.markdown.trim() && publication.markdown.length <= 5600 + && (0, sdd_diff_message_catalog_1.isSddDiffResolvedLocale)(publication.locale)) { + return [replyIntent({ ...context, locale: publication.locale }, correlationId, 'copilot-sdd-diff', Object.freeze({ kind: 'sdd-diff', markdown: publication.markdown }))]; + } if (kind === 'help' || kind === 'welcome') { const botLogin = typeof publication?.botLogin === 'string' && publication.botLogin.trim() ? publication.botLogin.trim() @@ -46417,6 +46619,8 @@ function renderSemanticReply(intent, catalog = (0, publication_message_catalog_1 return `${marker}\n\n${body}${operation ? `\n\n${operation}` : ''}`; } function renderReplyBody(intent, catalog) { + if (intent.projection.kind === 'sdd-diff') + return (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.markdown); if (intent.projection.kind === 'history-command') return (0, copilot_history_presentation_policy_1.renderCopilotHistory)(intent.projection, catalog); if (intent.projection.kind === 'fix-result') @@ -55431,6 +55635,8 @@ async function runDoUserRequestAction(param, options, intentPayload) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; +const sdd_diff_presentation_policy_1 = __nccwpck_require__(63302); +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const read_copilot_doctor_workflow_1 = __nccwpck_require__(12416); const read_copilot_history_workflow_1 = __nccwpck_require__(8140); const result_1 = __nccwpck_require__(73817); @@ -55454,6 +55660,15 @@ async function runExplicitCommentCommand(param, options, command, actorAuthoriza })]; if (command.name === 'help') return runHelpCommand(param, options); + if (command.name === 'sdd-diff') { + const snapshot = param.sddDiff && options.sddDiffQueryPort + ? await options.sddDiffQueryPort.read(param.sddDiff) : { state: 'unavailable' }; + const catalog = (0, sdd_diff_message_catalog_1.resolveSddDiffCatalog)(param.language.locale); + const markdown = (0, sdd_diff_presentation_policy_1.renderSddDiff)(snapshot, { owner: param.status.owner, repository: param.status.repository, + serverOrigin: options.sddDiffQueryPort?.serverOrigin ?? 'https://github.com' }, catalog); + return [new result_1.Result({ id: `${options.taskId}.SddDiff`, success: true, executed: true, stepFormat: 'markdown', + steps: [markdown], payload: Object.freeze({ publication: Object.freeze({ kind: 'sdd-diff', markdown, locale: catalog.locale }) }) })]; + } if (command.name === 'doctor') return [await (0, read_copilot_doctor_workflow_1.readCopilotDoctor)(param, options.taskId, options.doctorReadPorts)]; if (command.name === 'status') @@ -55716,6 +55931,7 @@ function projectCommentAutomationContext(source, language, userComment) { }), think: (0, think_workflow_1.projectThinkContext)(source), status: (0, status_command_policy_1.buildCopilotStatusSnapshot)(source), + ...projectSddDiffRequest(source), ...(source.issue?.number > 0 && !(source.pullRequest?.number > 0) ? { sdd: Object.freeze({ issueNumber: source.issue.number, admittedKind: issue_workflow_profile_1.ISSUE_WORKFLOW_KINDS.find(kind => kind === source.issueWorkflowKind), @@ -55773,6 +55989,29 @@ function withCommentLanguageAdaptation(context, adaptation) { }), }); } +/** Copies trusted workflow/profile facts; PR issue identity/author come only from the native link query. */ +function projectSddDiffRequest(source) { + const status = (0, status_command_policy_1.buildCopilotStatusSnapshot)(source); + const repositoryId = source.inputs?.repository?.id; + const number = status.target === 'pull-request' ? status.pullRequestNumber : status.issueNumber; + if (!Number.isSafeInteger(repositoryId) || !repositoryId || repositoryId < 1 || !number || number < 1 + || !source.issueWorkflowProfile || typeof source.issue.issueManagedBranches !== 'boolean' + || typeof source.preBranchSdd !== 'boolean') + return {}; + const list = (...values) => Object.freeze(values.filter((value) => typeof value === 'string')); + const labels = source.labels; + return { sddDiff: Object.freeze({ + target: Object.freeze({ kind: status.target, number }), repositoryId, + ...(status.target === 'issue' && source.inputs?.issue?.number === number + ? { issueAuthor: source.inputs.issue.user?.login } : {}), + profile: Object.freeze({ schemaVersion: source.issueWorkflowProfile.schemaVersion, enabled: Object.freeze([...source.issueWorkflowProfile.enabled]) }), + labelConfiguration: Object.freeze({ feature: list(labels.feature, labels.enhancement), bugfix: list(labels.bugfix, labels.bug), + documentation: list(labels.documentation, labels.docs), chore: list(labels.chore, labels.maintenance), + help: list(labels.help, labels.question), hotfix: list(labels.hotfix), release: list(labels.release) }), + profileDigest: source.issueWorkflowProfileDigest, trustedBotLogin: source.tokenUser?.trim() ?? '', + issueManagedBranches: source.issue.issueManagedBranches, preBranchSdd: source.preBranchSdd, baseBranch: source.branches?.development, + }) }; +} /***/ }), @@ -55896,7 +56135,8 @@ async function runCommentAutomation(initialParam, options, actorAuthorizationPor return [(0, comment_automation_command_workflow_1.invalidCommentCommandResult)(options.taskId, command.reason)]; } const isPublicMetadataCommand = command.kind === 'command' - && (command.command.name === 'help' || command.command.name === 'status' || command.command.name === 'doctor' || command.command.name === 'history'); + && (command.command.name === 'help' || command.command.name === 'status' || command.command.name === 'sdd-diff' + || command.command.name === 'doctor' || command.command.name === 'history'); if (!isPublicMetadataCommand && param.membersOnly && !await actorAuthorizationPort.isActorAllowedToUseMemberOnlyAutomation(param.actor)) { (0, logging_ports_1.logInfo)('Skipping agent automation because ai-members-only is enabled and the actor is not authorized.'); @@ -56536,7 +56776,7 @@ const pull_request_workflow_context_1 = __nccwpck_require__(73447); const pre_branch_sdd_1 = __nccwpck_require__(34730); const copilot_command_1 = __nccwpck_require__(11771); class IssueCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, historyReadPort, sddQueryPort, nativeStatusReadPort, doctorReadPorts) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, historyReadPort, sddQueryPort, sddDiffQueryPort, nativeStatusReadPort, doctorReadPorts) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -56554,6 +56794,7 @@ class IssueCommentUseCase { this.statusReadPorts = statusReadPorts; this.historyReadPort = historyReadPort; this.sddQueryPort = sddQueryPort; + this.sddDiffQueryPort = sddDiffQueryPort; this.nativeStatusReadPort = nativeStatusReadPort; this.doctorReadPorts = doctorReadPorts; this.taskId = "IssueCommentUseCase"; @@ -56607,6 +56848,7 @@ class IssueCommentUseCase { doctorReadPorts: this.doctorReadPorts, nativeStatusReadPort: this.nativeStatusReadPort, sddQueryPort: this.sddQueryPort, + sddDiffQueryPort: this.sddDiffQueryPort, syncBranchUseCase: this.syncBranchUseCase ? { invoke: (options) => this.syncBranchUseCase.invoke({ context: context.branchSync, options }) } : undefined, @@ -57272,7 +57514,7 @@ const check_pull_request_comment_language_use_case_1 = __nccwpck_require__(21729 const comment_automation_context_1 = __nccwpck_require__(37055); const pull_request_workflow_context_1 = __nccwpck_require__(73447); class PullRequestReviewCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts, historyReadPort, nativeStatusReadPort, doctorReadPorts) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts, historyReadPort, sddDiffQueryPort, nativeStatusReadPort, doctorReadPorts) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -57288,6 +57530,7 @@ class PullRequestReviewCommentUseCase { this.rememberBranchPorts = rememberBranchPorts; this.statusReadPorts = statusReadPorts; this.historyReadPort = historyReadPort; + this.sddDiffQueryPort = sddDiffQueryPort; this.nativeStatusReadPort = nativeStatusReadPort; this.doctorReadPorts = doctorReadPorts; this.taskId = "PullRequestReviewCommentUseCase"; @@ -57325,6 +57568,7 @@ class PullRequestReviewCommentUseCase { rememberBugbotRuleUseCase: this.rememberBugbotRuleUseCase, rememberBranchPorts: this.rememberBranchPorts, statusReadPorts: this.statusReadPorts, + sddDiffQueryPort: this.sddDiffQueryPort, doctorReadPorts: this.doctorReadPorts, nativeStatusReadPort: this.nativeStatusReadPort, syncBranchUseCase: this.syncBranchUseCase @@ -58647,6 +58891,191 @@ function canonicalIssueUrl(context) { } +/***/ }), + +/***/ 48684: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffQueryUseCase = void 0; +const issue_start_policy_1 = __nccwpck_require__(90332); +const issue_workflow_profile_1 = __nccwpck_require__(26744); +const pre_branch_sdd_diff_1 = __nccwpck_require__(28558); +/** Structural comparison over shared verified evidence, with no writer/model capability. */ +class SddDiffQueryUseCase { + constructor(ports) { + this.ports = Object.freeze({ + evidence: Object.freeze({ read: ports.evidence.read.bind(ports.evidence) }), + links: Object.freeze({ readClosingIssues: ports.links.readClosingIssues.bind(ports.links) }), + labels: Object.freeze({ getLabels: ports.labels.getLabels.bind(ports.labels) }), + descriptions: Object.freeze({ getDescription: ports.descriptions.getDescription.bind(ports.descriptions) }), + documents: Object.freeze({ readSdd: ports.documents.readSdd.bind(ports.documents) }), + }); + } + async read(request) { + try { + if (!positive(request.target.number) || !positive(request.repositoryId) + || !['issue', 'pull-request'].includes(request.target.kind)) + return { state: 'unavailable' }; + let links; + if (request.target.kind === 'pull-request') { + const association = await this.ports.links.readClosingIssues(request.target.number); + if (association.state !== 'verified' || association.pullNumber !== request.target.number + || association.issueNumbers.length > 100) + return { state: 'unavailable' }; + links = association; + } + const result = await this.readSnapshot(request, links); + // Every projection, including pending, absent and exempt, belongs to the + // association observed at admission. A moved association retains no old links. + if (links) { + const freshLinks = await this.ports.links.readClosingIssues(request.target.number); + if (freshLinks.state !== 'verified' || freshLinks.pullNumber !== request.target.number) + return { state: 'unavailable' }; + if (associationProof(freshLinks) !== associationProof(links)) + return { state: 'stale' }; + } + return Object.freeze(result); + } + catch { + return { state: 'unavailable' }; + } + } + async readSnapshot(request, links) { + let issueNumber = request.target.number; + let issueAuthor = request.issueAuthor; + if (links) { + if (links.issueNumbers.length === 0) + return { state: 'not-applicable' }; + const choice = await this.selectEligible(request, links); + if (choice.state !== 'selected') { + const freshChoice = await this.selectEligible(request, links); + return { state: freshChoice.state === choice.state ? choice.state : 'stale' }; + } + issueNumber = choice.context.issueNumber; + issueAuthor = choice.context.issueAuthor; + } + if (!issueAuthor?.trim()) + return { state: 'unavailable' }; + const context = await this.context(request, issueNumber, issueAuthor); + if (!context) + return { state: 'not-applicable' }; + const original = await this.ports.evidence.read(context); + const projection = projectEvidence(original, issueNumber); + if (original.state !== 'published' || !original.verified?.currentSha) + return projection; + if (!/^[a-f0-9]{64}$/u.test(original.authorityDigest ?? '')) + return { state: 'unavailable' }; + if (links && (links.headSha !== original.verified.branchHeadSha || links.headBranch !== original.verified.branchName)) { + return historicalStale(original, issueNumber); + } + const publication = publicationOf(original); + let result = { state: 'initial-only', publication }; + if (original.verified.currentSha !== original.verified.firstSha) { + const [before, after] = await Promise.all([ + this.ports.documents.readSdd(original.verified.firstSha, original.verified.path), + this.ports.documents.readSdd(original.verified.currentSha, original.verified.path), + ]); + if (before === undefined || after === undefined) + return { state: 'unavailable' }; + const changes = (0, pre_branch_sdd_diff_1.compareSddStructure)(before, after); + result = changes.state === 'bounded' + ? { state: 'bounded', publication, reason: changes.reason } + : { state: 'compared', publication, changes }; + } + // Re-admit the exact issue, then re-read the shared fenced authority. + let freshContext; + if (links) { + const choice = await this.selectEligible(request, links); + if (choice.state !== 'selected') + return { state: choice.state }; + if (choice.context.issueNumber !== issueNumber) + return historicalStale(original, issueNumber); + freshContext = choice.context; + } + else + freshContext = await this.context(request, issueNumber, issueAuthor); + if (!freshContext || freshContext.admittedKind !== context.admittedKind) + return { state: 'stale' }; + const fresh = await this.ports.evidence.read(freshContext); + if (!original.authorityDigest || fresh.authorityDigest !== original.authorityDigest + || proofOf(fresh) !== proofOf(original)) { + return fresh.state === 'published' ? historicalStale(fresh, issueNumber) : projectEvidence(fresh, issueNumber); + } + return result; + } + /** At most the adapter's fixed 100 native candidates; no body/number inference. */ + async selectEligible(request, links) { + let selected; + for (const number of links.issueNumbers) { + const author = links.issueAuthors[number]; + if (!author?.trim()) + throw new Error('Native issue author is unavailable.'); + const context = await this.context(request, number, author); + if (!context) + continue; + if (selected) + return { state: 'ambiguous' }; + selected = context; + } + return selected ? { state: 'selected', context: selected } : { state: 'not-applicable' }; + } + async context(request, issueNumber, issueAuthor) { + const [labels, body] = await Promise.all([ + this.ports.labels.getLabels(issueNumber), this.ports.descriptions.getDescription(issueNumber), + ]); + if (body === undefined) + throw new Error('Issue admission is unavailable.'); + const admission = (0, issue_workflow_profile_1.classifyIssueWorkflow)(labels, request.profile, request.labelConfiguration, body); + return admission.status === 'eligible' && (0, issue_start_policy_1.decideIssueStart)({ kind: admission.kind, labels, + issueManagedBranches: request.issueManagedBranches, preBranchSdd: request.preBranchSdd }).sddRequired ? Object.freeze({ issueNumber, admittedKind: admission.kind, issueAuthor, + profileDigest: request.profileDigest, trustedBotLogin: request.trustedBotLogin, + issueManagedBranches: request.issueManagedBranches, preBranchSdd: request.preBranchSdd, baseBranch: request.baseBranch }) : undefined; + } +} +exports.SddDiffQueryUseCase = SddDiffQueryUseCase; +function projectEvidence(evidence, issueNumber) { + if (evidence.state === 'not-required') + return { state: 'not-applicable' }; + if (evidence.state === 'absent') + return { state: 'no-publication' }; + if (evidence.state === 'invalid' || evidence.state === 'unavailable') + return { state: evidence.state }; + const publication = publicationOf(evidence); + if (evidence.state === 'stale') + return historicalStale(evidence, issueNumber); + if (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') { + return publication ? { state: 'pending-revision', publication, ...cardOf(evidence, issueNumber) } : { state: 'no-publication' }; + } + if (!publication?.currentSha) + return { state: 'invalid' }; + return publication.currentSha === publication.firstSha ? { state: 'initial-only', publication } : { state: 'unavailable' }; +} +function publicationOf(evidence) { + return evidence.verified ? Object.freeze({ path: evidence.verified.path, firstSha: evidence.verified.firstSha, + ...(evidence.verified.currentSha ? { currentSha: evidence.verified.currentSha } : {}) }) : undefined; +} +function historicalStale(evidence, issueNumber) { + const publication = publicationOf(evidence); + return { state: 'stale', ...(publication ? { publication } : {}), ...cardOf(evidence, issueNumber) }; +} +function cardOf(evidence, issueNumber) { + return positive(evidence.cardId) ? { card: Object.freeze({ issueNumber, commentId: evidence.cardId }) } : {}; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function proofOf(evidence) { + const verified = evidence.verified; + return JSON.stringify([evidence.state, evidence.authorityDigest, evidence.cardId, evidence.path, evidence.pendingQuestionIds, + verified?.path, verified?.capabilityId, verified?.branchName, verified?.branchHeadSha, verified?.firstSha, verified?.currentSha]); +} +function associationProof(links) { + return JSON.stringify([links.pullNumber, links.headSha, links.headBranch, + links.issueNumbers.map(number => [number, links.issueAuthors[number].toLowerCase()])]); +} + + /***/ }), /***/ 39893: @@ -58656,6 +59085,7 @@ function canonicalIssueUrl(context) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SddEvidenceQueryUseCase = void 0; +const node_crypto_1 = __nccwpck_require__(6005); const issue_start_policy_1 = __nccwpck_require__(90332); const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_evidence_read_ports_1 = __nccwpck_require__(94752); @@ -58689,7 +59119,10 @@ class SddEvidenceQueryUseCase { if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified, historical)) return { state: 'stale' }; - return Object.freeze({ ...result, ...(historical ? { historical: true } : {}) }); + return Object.freeze({ ...result, ...(!['invalid', 'unavailable'].includes(result.state) ? { + authorityDigest: (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([cardId, selection.body, digest, canonicalLabels(facts.labels), + record.phase === 'awaiting-answer' ? facts.comments : null])).digest('hex'), + } : {}), ...(historical ? { historical: true } : {}) }); } catch { return { state: 'unavailable' }; @@ -71608,7 +72041,7 @@ function containsCredentialMaterial(value, insideStorage = false) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DryRunSetupPlanConfirmation = exports.SetupPlanConfirmationAdapter = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_questionnaire_policy_1 = __nccwpck_require__(6009); class SetupPlanConfirmationAdapter { constructor(terminal, assumeYes) { @@ -71741,7 +72174,7 @@ async function collectSetupCredentials(input) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupCredentialPromptAdapter = exports.SetupTerminalCancelledError = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_token_permission_presenter_1 = __nccwpck_require__(63206); const setup_interaction_cancelled_error_1 = __nccwpck_require__(38313); const setup_workflow_pat_storage_copy_1 = __nccwpck_require__(97798); @@ -72019,7 +72452,7 @@ exports.SetupDoctorPresenter = void 0; exports.renderDoctorReport = renderDoctorReport; exports.doctorCheckLabel = doctorCheckLabel; const setup_doctor_message_catalog_1 = __nccwpck_require__(80226); -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); class SetupDoctorPresenter { constructor(catalog = (0, setup_doctor_message_catalog_1.resolveStaticSetupDoctorCatalog)()) { this.catalog = catalog; @@ -72102,7 +72535,7 @@ function doctorCheckLabel(id, catalog = (0, setup_doctor_message_catalog_1.resol Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ConsoleSetupJourneyPresenter = void 0; exports.renderSetupJourney = renderSetupJourney; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); class ConsoleSetupJourneyPresenter { present(view) { console.log(renderSetupJourney(view)); @@ -72362,7 +72795,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ConsoleSetupPlanPresenter = void 0; exports.renderSetupPlan = renderSetupPlan; const setup_configuration_policy_1 = __nccwpck_require__(56637); -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const pull_request_approval_policy_1 = __nccwpck_require__(98820); const setup_workflow_pat_storage_copy_1 = __nccwpck_require__(97798); class ConsoleSetupPlanPresenter { @@ -72440,7 +72873,7 @@ function storageLabel(policy) { /***/ }), -/***/ 83434: +/***/ 76673: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; @@ -72594,7 +73027,7 @@ function stripAnsi(value) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.ConsoleSetupQuestionRenderer = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_questionnaire_policy_1 = __nccwpck_require__(6009); const setup_question_guidance_policy_1 = __nccwpck_require__(42775); class ConsoleSetupQuestionRenderer { @@ -73141,7 +73574,7 @@ exports.renderSetupTokenPermissionSummary = renderSetupTokenPermissionSummary; exports.renderSetupTokenPermissionRequirements = renderSetupTokenPermissionRequirements; exports.renderSetupTokenPermissionReport = renderSetupTokenPermissionReport; const node_process_1 = __nccwpck_require__(97742); -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_permission_summary_policy_1 = __nccwpck_require__(10267); class ConsoleSetupTokenPermissionPresenter { constructor(mode = 'full') { @@ -73290,7 +73723,7 @@ function workflowPatStorageCopy(storage) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SetupWorkflowUpdatePromptAdapter = void 0; -const setup_prompt_rendering_1 = __nccwpck_require__(83434); +const setup_prompt_rendering_1 = __nccwpck_require__(76673); const setup_credential_prompt_adapter_1 = __nccwpck_require__(93232); class SetupWorkflowUpdatePromptAdapter { constructor(terminal) { @@ -84614,6 +85047,85 @@ class RepositorySecretsCommandRepository { exports.RepositorySecretsCommandRepository = RepositorySecretsCommandRepository; +/***/ }), + +/***/ 85885: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffLinkRepository = void 0; +const QUERY = `query SddDiffClosingIssues($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + databaseId nameWithOwner + pullRequest(number: $number) { + number headRefOid headRefName headRepository { databaseId nameWithOwner } repository { databaseId nameWithOwner } + closingIssuesReferences(first: 100) { + totalCount pageInfo { hasNextPage } + nodes { number author { login } repository { databaseId nameWithOwner } } + } + } + } +}`; +/** One bounded native read; absent, incomplete or conflicting identities never become guessed links. */ +class SddDiffLinkRepository { + constructor(client, binding) { + this.client = client; + this.binding = Object.freeze({ ...binding }); + } + async readClosingIssues(pullNumber) { + const unavailable = Object.freeze({ state: 'unavailable' }); + const binding = this.binding; + if (!positive(pullNumber) || !positive(binding.repositoryId) + || ![binding.owner, binding.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return unavailable; + try { + const response = object(await this.client.getClient(binding.token).graphql(QUERY, { + owner: binding.owner, repo: binding.repository, number: pullNumber, + })); + const repository = object(response?.repository); + const pull = object(repository?.pullRequest); + const links = object(pull?.closingIssuesReferences); + const page = object(links?.pageInfo); + if (!sameRepository(repository, binding) || !sameRepository(object(pull?.repository), binding) + || !sameRepository(object(pull?.headRepository), binding) + || typeof pull?.headRefName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,249}$/u.test(pull.headRefName) + || pull?.number !== pullNumber || typeof pull.headRefOid !== 'string' || !/^[a-f0-9]{40}$/u.test(pull.headRefOid) + || page?.hasNextPage !== false || !Array.isArray(links?.nodes) + || !Number.isSafeInteger(links.totalCount) || links.totalCount !== links.nodes.length || links.nodes.length > 100) + return unavailable; + const numbers = []; + const authors = {}; + for (const value of links.nodes) { + const issue = object(value); + const author = object(issue?.author); + if (typeof author?.login !== 'string' || !author.login.trim() || author.login.length > 100 + || !positive(issue?.number) || !sameRepository(object(issue?.repository), binding) + || numbers.includes(issue.number)) + return unavailable; + numbers.push(issue.number); + authors[issue.number] = author.login; + } + return Object.freeze({ state: 'verified', pullNumber, headSha: pull.headRefOid, headBranch: pull.headRefName, + issueNumbers: Object.freeze(numbers.sort((left, right) => left - right)), issueAuthors: Object.freeze(authors) }); + } + catch { + return unavailable; + } + } +} +exports.SddDiffLinkRepository = SddDiffLinkRepository; +function object(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function sameRepository(value, binding) { + return value?.databaseId === binding.repositoryId && typeof value.nameWithOwner === 'string' + && value.nameWithOwner.toLowerCase() === `${binding.owner}/${binding.repository}`.toLowerCase(); +} + + /***/ }), /***/ 40941: @@ -85680,6 +86192,7 @@ exports.COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'sdd-diff', 'doctor', 'history', 'description', @@ -85743,6 +86256,8 @@ function parseCopilotCommand(raw) { && name !== 'remember') { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } + if (name === 'sdd-diff' && argumentsList.length > 0) + return { kind: 'invalid', reason: '/copilot sdd-diff does not accept arguments.' }; if (name === 'doctor' && argumentsList.length > 0) { return { kind: 'invalid', reason: '/copilot doctor does not accept arguments.' }; } @@ -87946,6 +88461,153 @@ function isRecord(value) { } +/***/ }), + +/***/ 28558: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SDD_DIFF_LIMITS = void 0; +exports.compareSddStructure = compareSddStructure; +exports.SDD_DIFF_LIMITS = Object.freeze({ + characters: 70000, bytes: 280000, lines: 2000, comparisons: 2000000, + sectionTitles: 8, titleCharacters: 120, +}); +class ComparisonBudgetExceeded extends Error { +} +/** Exact line counts with Hirschberg's linear-memory LCS; every equality consumes bounded work. */ +function compareSddStructure(before, after) { + for (const text of [before, after]) { + if (Buffer.byteLength(text, 'utf8') > exports.SDD_DIFF_LIMITS.bytes) + return Object.freeze({ state: 'bounded', reason: 'bytes' }); + if (text.length > exports.SDD_DIFF_LIMITS.characters) + return Object.freeze({ state: 'bounded', reason: 'characters' }); + } + const left = documentLines(before); + const right = documentLines(after); + if (left.length > exports.SDD_DIFF_LIMITS.lines || right.length > exports.SDD_DIFF_LIMITS.lines) { + return Object.freeze({ state: 'bounded', reason: 'lines' }); + } + const leftMatches = new Uint8Array(left.length); + const rightMatches = new Uint8Array(right.length); + let comparisons = 0; + let common = 0; + const equal = (a, b) => { + if (comparisons === exports.SDD_DIFF_LIMITS.comparisons) + throw new ComparisonBudgetExceeded(); + comparisons++; + return a === b; + }; + const match = (a, b) => { leftMatches[a] = 1; rightMatches[b] = 1; common++; }; + const row = (ls, le, rs, re, reverse) => { + let previous = new Uint16Array(re - rs + 1); + let current = new Uint16Array(re - rs + 1); + for (let i = 0; i < le - ls; i++) { + current[0] = 0; + for (let j = 1; j <= re - rs; j++) { + current[j] = equal(left[reverse ? le - i - 1 : ls + i], right[reverse ? re - j : rs + j - 1]) + ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]); + } + [previous, current] = [current, previous]; + } + return previous; + }; + // Row arrays leave scope before recursion; no quadratic trace is retained. + const split = (ls, middle, le, rs, re) => { + const forward = row(ls, middle, rs, re, false); + const backward = row(middle, le, rs, re, true); + let best = -1; + let offset = 0; + for (let j = 0; j <= re - rs; j++) { + const score = forward[j] + backward[re - rs - j]; + if (score > best) { + best = score; + offset = j; + } + } + return rs + offset; + }; + const align = (ls, le, rs, re) => { + if (ls === le || rs === re) + return; + if (le - ls === 1) { + for (let j = rs; j < re; j++) + if (equal(left[ls], right[j])) { + match(ls, j); + break; + } + return; + } + const middle = ls + Math.floor((le - ls) / 2); + const rightMiddle = split(ls, middle, le, rs, re); + align(ls, middle, rs, rightMiddle); + align(middle, le, rightMiddle, re); + }; + try { + let start = 0; + let leftEnd = left.length; + let rightEnd = right.length; + while (start < leftEnd && start < rightEnd && equal(left[start], right[start])) { + match(start, start); + start++; + } + while (start < leftEnd && start < rightEnd && equal(left[leftEnd - 1], right[rightEnd - 1])) { + match(--leftEnd, --rightEnd); + } + align(start, leftEnd, start, rightEnd); + } + catch (error) { + if (!(error instanceof ComparisonBudgetExceeded)) + throw error; + return Object.freeze({ state: 'bounded', reason: 'work' }); + } + const titles = new Set(); + collectChangedSections(right, rightMatches, titles); + collectChangedSections(left, leftMatches, titles); + return Object.freeze({ state: 'compared', addedLines: right.length - common, removedLines: left.length - common, + sections: Object.freeze([...titles].slice(0, exports.SDD_DIFF_LIMITS.sectionTitles).map(boundedTitle)), + omittedSections: Math.max(0, titles.size - exports.SDD_DIFF_LIMITS.sectionTitles) }); +} +/** CRLF and terminal newline markers do not count as added/removed content lines. */ +function documentLines(text) { + if (!text) + return []; + const lines = text.split(/\r?\n/u); + if (text.endsWith('\n')) + lines.pop(); + return lines; +} +function collectChangedSections(lines, matches, titles) { + let fence; + let title; + for (let index = 0; index < lines.length; index++) { + const boundary = lines[index].match(/^ {0,3}(`{3,}|~{3,})(.*)$/u); + if (boundary) { + if (!fence && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) + fence = boundary[1]; + else if (fence && boundary[1][0] === fence[0] && boundary[1].length >= fence.length + && /^[ \t]*$/u.test(boundary[2])) + fence = undefined; + } + else if (!fence) { + const heading = lines[index].match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/u); + if (heading) + title = heading[1]; + } + if (matches[index] === 0 && title) + titles.add(title); + } +} +function boundedTitle(title) { + let value = title.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').slice(0, exports.SDD_DIFF_LIMITS.titleCharacters); + if (/[\uD800-\uDBFF]$/u.test(value)) + value = value.slice(0, -1); + return value; +} + + /***/ }), /***/ 8024: @@ -90975,6 +91637,7 @@ exports.createPullRequestReviewCommentUseCaseCompositionRoot = createPullRequest exports.createCommitUseCaseCompositionRoot = createCommitUseCaseCompositionRoot; exports.createMainRunRouteCompositionRoot = createMainRunRouteCompositionRoot; exports.createClosedIssueRetirementUseCase = createClosedIssueRetirementUseCase; +const sdd_diff_composition_root_1 = __nccwpck_require__(83434); const pull_request_status_composition_root_1 = __nccwpck_require__(49891); const github_runtime_doctor_composition_root_1 = __nccwpck_require__(36459); const reply_request_composition_root_1 = __nccwpck_require__(99065); @@ -91101,7 +91764,7 @@ function createIssueCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); } function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); @@ -91117,7 +91780,7 @@ function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); } function createCommitUseCaseCompositionRoot(projectBoardCommandPort, binding) { return new commit_use_case_1.CommitUseCase(new notify_new_commit_on_issue_use_case_1.NotifyNewCommitOnIssueUseCase((0, push_single_action_capability_port_binding_1.bindIssueReopen)((0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), binding)), new check_changes_issue_size_use_case_1.CheckChangesIssueSizeUseCase((0, lifecycle_capability_port_binding_2.bindProjectBoardCommands)(projectBoardCommandPort, binding), (0, lifecycle_capability_port_binding_2.bindIssueLabels)((0, issue_labels_composition_root_1.createIssueLabelRepository)(), binding), (0, push_single_action_capability_port_binding_1.bindPullRequestBranchQuery)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchChangeSize)(new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), binding)), createDetectPotentialProblemsUseCase(binding), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(binding), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)()); @@ -91620,6 +92283,38 @@ function createReplyRequestReadPort(binding) { } +/***/ }), + +/***/ 83434: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSddDiffQueryPort = createSddDiffQueryPort; +const sdd_evidence_query_use_case_1 = __nccwpck_require__(39893); +const sdd_diff_query_use_case_1 = __nccwpck_require__(48684); +const sdd_diff_link_repository_1 = __nccwpck_require__(85885); +const pre_branch_sdd_workspace_adapter_1 = __nccwpck_require__(35849); +const sdd_evidence_composition_root_1 = __nccwpck_require__(51671); +const github_project_client_factory_1 = __nccwpck_require__(23691); +/** No agent, provisioning, approval or lifecycle writer is supplied. */ +function createSddDiffQueryPort(binding, comments) { + const bound = Object.freeze({ ...binding }); + const reads = (0, sdd_evidence_composition_root_1.createSddEvidenceReadPorts)(bound, comments); + const evidence = new sdd_evidence_query_use_case_1.SddEvidenceQueryUseCase(reads); + const documents = new pre_branch_sdd_workspace_adapter_1.PreBranchSddWorkspaceAdapter(process.cwd(), bound.token); + const client = (0, github_project_client_factory_1.createGraphqlTransportClient)(); + return Object.freeze({ serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com', + read: (request) => new sdd_diff_query_use_case_1.SddDiffQueryUseCase({ evidence: Object.freeze({ read: evidence.read.bind(evidence) }), + links: new sdd_diff_link_repository_1.SddDiffLinkRepository(client, { ...bound, repositoryId: request.repositoryId }), + labels: reads.labels, descriptions: reads.descriptions, + documents: Object.freeze({ readSdd: documents.readSdd.bind(documents) }), + }).read(request), + }); +} + + /***/ }), /***/ 51671: diff --git a/build/github_action/index.js b/build/github_action/index.js index e9b6c459d..678921b88 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -39340,7 +39340,7 @@ exports.shouldTrackAgentActivity = shouldTrackAgentActivity; const agent_1 = __nccwpck_require__(89040); const copilot_command_1 = __nccwpck_require__(11771); const branch_sync_command_1 = __nccwpck_require__(51114); -const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'doctor', 'history', 'dismiss', 'remember']); +const DETERMINISTIC_COMMENT_COMMANDS = new Set(['help', 'status', 'sdd-diff', 'doctor', 'history', 'dismiss', 'remember']); /** Decides whether a route can invoke an agent for its current event. */ function shouldTrackAgentActivity(execution, route) { if (!hasTarget(execution)) @@ -43400,6 +43400,7 @@ const READ_ONLY_COMMANDS = Object.freeze([ ['/copilot recheck', 'interaction.help.command.recheck'], ['/copilot description', 'interaction.help.command.description'], ['/copilot status', 'interaction.help.command.status'], + ['/copilot sdd-diff', 'interaction.help.command.sddDiff'], ['/copilot doctor', 'interaction.help.command.doctor'], ['/copilot history', 'interaction.help.command.history'], ]); @@ -45859,6 +45860,7 @@ const PUBLICATION_SURFACE_MESSAGE_IDS = Object.freeze([ 'interaction.help.command.recheck', 'interaction.help.command.description', 'interaction.help.command.status', + 'interaction.help.command.sddDiff', 'interaction.help.command.doctor', 'interaction.help.command.history', 'interaction.history.fixPushed', @@ -46062,6 +46064,7 @@ const ENGLISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 're-run the review and reconcile findings.', 'interaction.help.command.description': 'refresh the pull-request description.', 'interaction.help.command.status': 'show the current automation status.', + 'interaction.help.command.sddDiff': 'compare verified first and current SDD publications without a model.', 'interaction.help.command.doctor': 'check available runtime prerequisites without making repository changes.', 'interaction.help.command.history': 'show up to ten supported operations and their verified receipts.', 'interaction.history.fixPushed': 'Fix changes were pushed. Findings require an independent review of the pushed revision.', @@ -46274,6 +46277,7 @@ const SPANISH_MESSAGES = Object.freeze({ 'interaction.help.command.recheck': 'repite la revisión y reconcilia los hallazgos.', 'interaction.help.command.description': 'actualiza la descripción del pull request.', 'interaction.help.command.status': 'muestra el estado actual de la automatización.', + 'interaction.help.command.sddDiff': 'compara las publicaciones inicial y actual verificadas del SDD sin modelo.', 'interaction.help.command.doctor': 'comprueba los requisitos disponibles del runtime sin modificar el repositorio.', 'interaction.help.command.history': 'muestra hasta diez operaciones compatibles y sus recibos verificables.', 'interaction.history.fixPushed': 'Se publicaron los cambios de la corrección. Los hallazgos requieren una revisión independiente de esa revisión.', @@ -47546,6 +47550,198 @@ function calculateReviewersStillNeeded(desiredCount, currentCount, confirmedCoun } +/***/ }), + +/***/ 2230: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPANISH_SDD_DIFF_DEFINITION = exports.ENGLISH_SDD_DIFF_DEFINITION = exports.SDD_DIFF_MESSAGE_IDS = void 0; +exports.resolveSddDiffCatalog = resolveSddDiffCatalog; +exports.isSddDiffResolvedLocale = isSddDiffResolvedLocale; +const message_catalog_1 = __nccwpck_require__(27097); +const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); +exports.SDD_DIFF_MESSAGE_IDS = Object.freeze([ + 'sddDiff.heading', 'sddDiff.status', 'sddDiff.action', 'sddDiff.not-applicable', 'sddDiff.no-publication', + 'sddDiff.initial-only', 'sddDiff.stale', 'sddDiff.pending-revision', 'sddDiff.invalid', 'sddDiff.unavailable', + 'sddDiff.ambiguous', 'sddDiff.bounded', 'sddDiff.compared', 'sddDiff.none', 'sddDiff.publish', 'sddDiff.refresh', + 'sddDiff.answer', 'sddDiff.retry', 'sddDiff.choose', 'sddDiff.reduce', 'sddDiff.first', 'sddDiff.current', + 'sddDiff.historical', 'sddDiff.historicalCurrent', 'sddDiff.card', 'sddDiff.commit', 'sddDiff.compare', 'sddDiff.lines', 'sddDiff.sections', 'sddDiff.omitted', 'sddDiff.structural', + 'sddDiff.charactersLimit', 'sddDiff.bytesLimit', 'sddDiff.linesLimit', 'sddDiff.workLimit', +]); +const EN = Object.freeze({ + 'sddDiff.heading': 'SDD structural comparison', 'sddDiff.status': 'Current status', 'sddDiff.action': 'Next action', + 'sddDiff.not-applicable': 'An SDD comparison is not applicable to this target.', + 'sddDiff.no-publication': 'No verified SDD publication is available yet.', + 'sddDiff.initial-only': 'Only the first publication is verified; there is no revision pair to compare.', + 'sddDiff.stale': 'Current issue, SDD or branch evidence changed or disagrees. Historical evidence does not certify a current comparison.', + 'sddDiff.pending-revision': 'The requested SDD revision is pending. Current change counts are unavailable.', + 'sddDiff.invalid': 'The owning SDD evidence could not be verified.', + 'sddDiff.unavailable': 'The comparison could not be verified. No current change counts are available.', + 'sddDiff.ambiguous': 'More than one SDD-eligible issue is linked to this PR.', + 'sddDiff.bounded': 'The verified documents exceed a fixed comparison limit; exact counts are unavailable.', + 'sddDiff.compared': 'Compared verified published revisions.', 'sddDiff.none': 'No action is required.', + 'sddDiff.publish': 'Publish the SDD through the existing issue workflow, then retry this command.', + 'sddDiff.refresh': 'Retry this command with current evidence; if the issue changed, resume its existing SDD clarification workflow.', + 'sddDiff.answer': 'Answer the existing SDD questions or resume publication, then retry this command.', + 'sddDiff.retry': 'Inspect the existing SDD evidence and read access, then retry this command.', + 'sddDiff.choose': 'Run this command in the intended issue conversation.', + 'sddDiff.reduce': 'Keep the SDD within the documented fixed limits, then retry this command.', + 'sddDiff.first': 'First publication', 'sddDiff.current': 'Current publication', 'sddDiff.historical': 'Historical first publication', 'sddDiff.historicalCurrent': 'Historical published revision', + 'sddDiff.card': 'SDD clarification card', 'sddDiff.commit': 'Publication commit', 'sddDiff.compare': 'Compare publication commits', + 'sddDiff.lines': 'Added lines: {added}; removed lines: {removed}.', 'sddDiff.sections': 'Changed section titles', + 'sddDiff.omitted': 'Additional changed titles omitted: {count}.', + 'sddDiff.charactersLimit': 'Limit: 70,000 characters per document.', 'sddDiff.bytesLimit': 'Limit: 280,000 UTF-8 bytes per document.', + 'sddDiff.linesLimit': 'Limit: 2,000 content lines per document.', 'sddDiff.workLimit': 'Limit: 2,000,000 line-comparison steps.', + 'sddDiff.structural': 'This compares structure and content lines; it does not assess semantic requirements.', +}); +const ES = Object.freeze({ + 'sddDiff.heading': 'Comparación estructural del SDD', 'sddDiff.status': 'Estado actual', 'sddDiff.action': 'Siguiente acción', + 'sddDiff.not-applicable': 'La comparación de SDD no corresponde a este destino.', + 'sddDiff.no-publication': 'Todavía no hay una publicación de SDD verificada.', + 'sddDiff.initial-only': 'Sólo está verificada la primera publicación; no hay dos revisiones que comparar.', + 'sddDiff.stale': 'La evidencia de la issue, del SDD o de la rama cambió o no coincide. La evidencia histórica no certifica una comparación actual.', + 'sddDiff.pending-revision': 'La revisión solicitada del SDD está pendiente. No hay recuentos de cambios actuales.', + 'sddDiff.invalid': 'No se pudo verificar la evidencia del SDD propietario.', + 'sddDiff.unavailable': 'No se pudo verificar la comparación. No hay recuentos de cambios actuales.', + 'sddDiff.ambiguous': 'Esta PR enlaza más de una issue apta para comparar SDD.', + 'sddDiff.bounded': 'Los documentos verificados superan un límite fijo de comparación; no hay recuentos exactos.', + 'sddDiff.compared': 'Se compararon las revisiones publicadas y verificadas.', 'sddDiff.none': 'No se requiere ninguna acción.', + 'sddDiff.publish': 'Publica el SDD mediante el flujo existente de la issue y vuelve a ejecutar el comando.', + 'sddDiff.refresh': 'Vuelve a ejecutar el comando con evidencia actual; si cambió la issue, retoma su flujo existente de aclaraciones del SDD.', + 'sddDiff.answer': 'Responde las preguntas existentes del SDD o retoma su publicación y vuelve a ejecutar el comando.', + 'sddDiff.retry': 'Revisa la evidencia existente del SDD y los permisos de lectura y vuelve a ejecutar el comando.', + 'sddDiff.choose': 'Ejecuta el comando en la conversación de la issue deseada.', + 'sddDiff.reduce': 'Mantén el SDD dentro de los límites fijos documentados y vuelve a ejecutar el comando.', + 'sddDiff.first': 'Primera publicación', 'sddDiff.current': 'Publicación actual', 'sddDiff.historical': 'Primera publicación histórica', 'sddDiff.historicalCurrent': 'Revisión publicada histórica', + 'sddDiff.card': 'Tarjeta de aclaraciones del SDD', 'sddDiff.commit': 'Commit de publicación', 'sddDiff.compare': 'Comparar commits de publicación', + 'sddDiff.lines': 'Líneas añadidas: {added}; líneas eliminadas: {removed}.', 'sddDiff.sections': 'Títulos de secciones modificadas', + 'sddDiff.omitted': 'Títulos modificados adicionales omitidos: {count}.', + 'sddDiff.charactersLimit': 'Límite: 70.000 caracteres por documento.', 'sddDiff.bytesLimit': 'Límite: 280.000 bytes UTF-8 por documento.', + 'sddDiff.linesLimit': 'Límite: 2.000 líneas de contenido por documento.', 'sddDiff.workLimit': 'Límite: 2.000.000 pasos de comparación de líneas.', + 'sddDiff.structural': 'La comparación incluye estructura y líneas de contenido; no evalúa requisitos semánticos.', +}); +exports.ENGLISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'en-US', compatibleBaseLanguage: 'en', messages: EN, +}); +exports.SPANISH_SDD_DIFF_DEFINITION = Object.freeze({ + version: message_catalog_1.MESSAGE_CATALOG_VERSION, locale: 'es-ES', compatibleBaseLanguage: 'es', messages: ES, +}); +function resolveSddDiffCatalog(locale) { + return (0, resolved_message_catalog_policy_1.resolveStaticMessageCatalogView)(locale, exports.ENGLISH_SDD_DIFF_DEFINITION, [exports.ENGLISH_SDD_DIFF_DEFINITION, exports.SPANISH_SDD_DIFF_DEFINITION]); +} +/** Locale admission belongs to the complete bundled catalog boundary. */ +function isSddDiffResolvedLocale(value) { + return value === 'en-US' || value === 'es-ES'; +} + + +/***/ }), + +/***/ 63302: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.renderSddDiff = renderSddDiff; +const pre_branch_sdd_1 = __nccwpck_require__(34730); +const STATES = ['not-applicable', 'no-publication', 'initial-only', 'stale', 'pending-revision', 'invalid', 'unavailable', 'ambiguous', 'bounded', 'compared']; +const ACTIONS = Object.freeze({ + 'not-applicable': 'sddDiff.none', 'no-publication': 'sddDiff.publish', 'initial-only': 'sddDiff.none', + stale: 'sddDiff.refresh', 'pending-revision': 'sddDiff.answer', invalid: 'sddDiff.retry', unavailable: 'sddDiff.retry', + ambiguous: 'sddDiff.choose', bounded: 'sddDiff.reduce', compared: 'sddDiff.none', +}); +/** Immutable links are constructed only from a bound server/repository and verified publication facts. */ +function renderSddDiff(snapshot, scope, catalog) { + const render = catalog.message; + const heading = `## ${render('sddDiff.heading')}`; + const fallback = `${heading}\n\n${render('sddDiff.unavailable')}\n\n**${render('sddDiff.action')}:** ${render('sddDiff.retry')}`; + const root = repositoryRoot(scope); + if (!root || !STATES.includes(snapshot.state)) + return fallback; + const lines = [heading, '', `**${render('sddDiff.status')}:** ${render(`sddDiff.${snapshot.state}`)}`, '', + `**${render('sddDiff.action')}:** ${render(ACTIONS[snapshot.state])}`]; + if ('publication' in snapshot && snapshot.publication) { + const publication = snapshot.publication; + if (!(0, pre_branch_sdd_1.isSafeSddPath)(publication.path) || !sha(publication.firstSha)) + return fallback; + const historical = snapshot.state === 'stale' || snapshot.state === 'pending-revision'; + if (historical) { + lines.push('', link('sddDiff.historical', publication.firstSha, publication.path)); + if (publication.currentSha !== undefined) { + if (!sha(publication.currentSha)) + return fallback; + if (publication.currentSha !== publication.firstSha) + lines.push(link('sddDiff.historicalCurrent', publication.currentSha, publication.path)); + } + } + else { + lines.push('', link('sddDiff.first', publication.firstSha, publication.path)); + if (snapshot.state !== 'initial-only') { + if (!sha(publication.currentSha) || publication.currentSha === publication.firstSha) + return fallback; + lines.push(link('sddDiff.current', publication.currentSha, publication.path)); + } + } + } + else if (snapshot.state === 'initial-only' || snapshot.state === 'compared' || snapshot.state === 'bounded') + return fallback; + if (snapshot.card && (snapshot.state === 'stale' || snapshot.state === 'pending-revision')) { + if (!positive(snapshot.card.issueNumber) || !positive(snapshot.card.commentId)) + return fallback; + lines.push('', `[${render('sddDiff.card')}](${root}/issues/${snapshot.card.issueNumber}#issuecomment-${snapshot.card.commentId})`); + } + if (snapshot.state === 'compared' || snapshot.state === 'bounded') { + const publication = snapshot.publication; + lines.push('', `[${render('sddDiff.compare')}](${root}/compare/${publication.firstSha}...${publication.currentSha})`); + } + if (snapshot.state === 'bounded') { + if (!['characters', 'bytes', 'lines', 'work'].includes(snapshot.reason)) + return fallback; + lines.push('', render(`sddDiff.${snapshot.reason}Limit`)); + } + if (snapshot.state === 'compared') { + const changes = snapshot.changes; + if (changes.state !== 'compared' || !count(changes.addedLines, 2000) || !count(changes.removedLines, 2000) + || !count(changes.omittedSections, 4000) || !Array.isArray(changes.sections) || changes.sections.length > 8 + || changes.sections.some(title => typeof title !== 'string' || title.length > 120)) + return fallback; + lines.push('', render('sddDiff.lines', { added: changes.addedLines, removed: changes.removedLines }), '', render('sddDiff.structural')); + if (changes.sections.length) + lines.push('', `**${render('sddDiff.sections')}:**`, ...changes.sections.map(title => `- ${escapeTitle(title)}`)); + if (changes.omittedSections) + lines.push('', render('sddDiff.omitted', { count: changes.omittedSections })); + } + const body = lines.join('\n'); + return body.length <= 5600 ? body : fallback; + function link(label, commit, path) { + return `[${render(label)} · ${commit.slice(0, 9)}](${root}/blob/${commit}/${path}) · [${render('sddDiff.commit')}](${root}/commit/${commit})`; + } +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function count(value, max) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= max; } +function sha(value) { return typeof value === 'string' && /^[a-f0-9]{40}$/u.test(value); } +function repositoryRoot(scope) { + if (scope.serverOrigin.length > 200 || ![scope.owner, scope.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return undefined; + try { + const url = new URL(scope.serverOrigin); + return url.protocol === 'https:' && !url.username && !url.password && url.pathname === '/' && !url.search && !url.hash + ? `${url.origin}/${scope.owner}/${scope.repository}` : undefined; + } + catch { + return undefined; + } +} +function escapeTitle(value) { + return value.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').replace(/@/gu, '@\u200b') + .replace(/([\\`*_{}[\]()<>#|~!])/gu, '\\$1'); +} + + /***/ }), /***/ 99167: @@ -47755,6 +47951,7 @@ exports.hasOwnedPrimaryIssuePublication = hasOwnedPrimaryIssuePublication; exports.selectSemanticReplyIntents = selectSemanticReplyIntents; exports.renderSemanticReply = renderSemanticReply; exports.renderSemanticStatus = renderSemanticStatus; +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const github_runtime_doctor_presentation_policy_1 = __nccwpck_require__(2808); const copilot_operation_1 = __nccwpck_require__(40221); const copilot_history_presentation_policy_1 = __nccwpck_require__(55507); @@ -47857,6 +48054,11 @@ function selectSemanticReplyIntents(context) { return [replyIntent(context, correlationId, 'direct-answer', directAnswer)]; const publication = (0, result_1.getResultPayload)(payload.publication); const kind = publication?.kind; + if (kind === 'sdd-diff' && result.id.endsWith('.SddDiff') + && typeof publication?.markdown === 'string' && publication.markdown.trim() && publication.markdown.length <= 5600 + && (0, sdd_diff_message_catalog_1.isSddDiffResolvedLocale)(publication.locale)) { + return [replyIntent({ ...context, locale: publication.locale }, correlationId, 'copilot-sdd-diff', Object.freeze({ kind: 'sdd-diff', markdown: publication.markdown }))]; + } if (kind === 'help' || kind === 'welcome') { const botLogin = typeof publication?.botLogin === 'string' && publication.botLogin.trim() ? publication.botLogin.trim() @@ -47912,6 +48114,8 @@ function renderSemanticReply(intent, catalog = (0, publication_message_catalog_1 return `${marker}\n\n${body}${operation ? `\n\n${operation}` : ''}`; } function renderReplyBody(intent, catalog) { + if (intent.projection.kind === 'sdd-diff') + return (0, github_comment_publication_policy_1.sanitizeAgentMarkdown)(intent.projection.markdown); if (intent.projection.kind === 'history-command') return (0, copilot_history_presentation_policy_1.renderCopilotHistory)(intent.projection, catalog); if (intent.projection.kind === 'fix-result') @@ -53852,6 +54056,8 @@ async function runDoUserRequestAction(param, options, intentPayload) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.runExplicitCommentCommand = runExplicitCommentCommand; exports.invalidCommentCommandResult = invalidCommentCommandResult; +const sdd_diff_presentation_policy_1 = __nccwpck_require__(63302); +const sdd_diff_message_catalog_1 = __nccwpck_require__(2230); const read_copilot_doctor_workflow_1 = __nccwpck_require__(12416); const read_copilot_history_workflow_1 = __nccwpck_require__(8140); const result_1 = __nccwpck_require__(73817); @@ -53875,6 +54081,15 @@ async function runExplicitCommentCommand(param, options, command, actorAuthoriza })]; if (command.name === 'help') return runHelpCommand(param, options); + if (command.name === 'sdd-diff') { + const snapshot = param.sddDiff && options.sddDiffQueryPort + ? await options.sddDiffQueryPort.read(param.sddDiff) : { state: 'unavailable' }; + const catalog = (0, sdd_diff_message_catalog_1.resolveSddDiffCatalog)(param.language.locale); + const markdown = (0, sdd_diff_presentation_policy_1.renderSddDiff)(snapshot, { owner: param.status.owner, repository: param.status.repository, + serverOrigin: options.sddDiffQueryPort?.serverOrigin ?? 'https://github.com' }, catalog); + return [new result_1.Result({ id: `${options.taskId}.SddDiff`, success: true, executed: true, stepFormat: 'markdown', + steps: [markdown], payload: Object.freeze({ publication: Object.freeze({ kind: 'sdd-diff', markdown, locale: catalog.locale }) }) })]; + } if (command.name === 'doctor') return [await (0, read_copilot_doctor_workflow_1.readCopilotDoctor)(param, options.taskId, options.doctorReadPorts)]; if (command.name === 'status') @@ -54137,6 +54352,7 @@ function projectCommentAutomationContext(source, language, userComment) { }), think: (0, think_workflow_1.projectThinkContext)(source), status: (0, status_command_policy_1.buildCopilotStatusSnapshot)(source), + ...projectSddDiffRequest(source), ...(source.issue?.number > 0 && !(source.pullRequest?.number > 0) ? { sdd: Object.freeze({ issueNumber: source.issue.number, admittedKind: issue_workflow_profile_1.ISSUE_WORKFLOW_KINDS.find(kind => kind === source.issueWorkflowKind), @@ -54194,6 +54410,29 @@ function withCommentLanguageAdaptation(context, adaptation) { }), }); } +/** Copies trusted workflow/profile facts; PR issue identity/author come only from the native link query. */ +function projectSddDiffRequest(source) { + const status = (0, status_command_policy_1.buildCopilotStatusSnapshot)(source); + const repositoryId = source.inputs?.repository?.id; + const number = status.target === 'pull-request' ? status.pullRequestNumber : status.issueNumber; + if (!Number.isSafeInteger(repositoryId) || !repositoryId || repositoryId < 1 || !number || number < 1 + || !source.issueWorkflowProfile || typeof source.issue.issueManagedBranches !== 'boolean' + || typeof source.preBranchSdd !== 'boolean') + return {}; + const list = (...values) => Object.freeze(values.filter((value) => typeof value === 'string')); + const labels = source.labels; + return { sddDiff: Object.freeze({ + target: Object.freeze({ kind: status.target, number }), repositoryId, + ...(status.target === 'issue' && source.inputs?.issue?.number === number + ? { issueAuthor: source.inputs.issue.user?.login } : {}), + profile: Object.freeze({ schemaVersion: source.issueWorkflowProfile.schemaVersion, enabled: Object.freeze([...source.issueWorkflowProfile.enabled]) }), + labelConfiguration: Object.freeze({ feature: list(labels.feature, labels.enhancement), bugfix: list(labels.bugfix, labels.bug), + documentation: list(labels.documentation, labels.docs), chore: list(labels.chore, labels.maintenance), + help: list(labels.help, labels.question), hotfix: list(labels.hotfix), release: list(labels.release) }), + profileDigest: source.issueWorkflowProfileDigest, trustedBotLogin: source.tokenUser?.trim() ?? '', + issueManagedBranches: source.issue.issueManagedBranches, preBranchSdd: source.preBranchSdd, baseBranch: source.branches?.development, + }) }; +} /***/ }), @@ -54317,7 +54556,8 @@ async function runCommentAutomation(initialParam, options, actorAuthorizationPor return [(0, comment_automation_command_workflow_1.invalidCommentCommandResult)(options.taskId, command.reason)]; } const isPublicMetadataCommand = command.kind === 'command' - && (command.command.name === 'help' || command.command.name === 'status' || command.command.name === 'doctor' || command.command.name === 'history'); + && (command.command.name === 'help' || command.command.name === 'status' || command.command.name === 'sdd-diff' + || command.command.name === 'doctor' || command.command.name === 'history'); if (!isPublicMetadataCommand && param.membersOnly && !await actorAuthorizationPort.isActorAllowedToUseMemberOnlyAutomation(param.actor)) { (0, logging_ports_1.logInfo)('Skipping agent automation because ai-members-only is enabled and the actor is not authorized.'); @@ -54992,7 +55232,7 @@ const pull_request_workflow_context_1 = __nccwpck_require__(73447); const pre_branch_sdd_1 = __nccwpck_require__(34730); const copilot_command_1 = __nccwpck_require__(11771); class IssueCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, historyReadPort, sddQueryPort, nativeStatusReadPort, doctorReadPorts) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, preBranchSddContinuation, rememberBranchPorts, statusReadPorts, historyReadPort, sddQueryPort, sddDiffQueryPort, nativeStatusReadPort, doctorReadPorts) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -55010,6 +55250,7 @@ class IssueCommentUseCase { this.statusReadPorts = statusReadPorts; this.historyReadPort = historyReadPort; this.sddQueryPort = sddQueryPort; + this.sddDiffQueryPort = sddDiffQueryPort; this.nativeStatusReadPort = nativeStatusReadPort; this.doctorReadPorts = doctorReadPorts; this.taskId = "IssueCommentUseCase"; @@ -55063,6 +55304,7 @@ class IssueCommentUseCase { doctorReadPorts: this.doctorReadPorts, nativeStatusReadPort: this.nativeStatusReadPort, sddQueryPort: this.sddQueryPort, + sddDiffQueryPort: this.sddDiffQueryPort, syncBranchUseCase: this.syncBranchUseCase ? { invoke: (options) => this.syncBranchUseCase.invoke({ context: context.branchSync, options }) } : undefined, @@ -55856,7 +56098,7 @@ const check_pull_request_comment_language_use_case_1 = __nccwpck_require__(21729 const comment_automation_context_1 = __nccwpck_require__(37055); const pull_request_workflow_context_1 = __nccwpck_require__(73447); class PullRequestReviewCommentUseCase { - constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts, historyReadPort, nativeStatusReadPort, doctorReadPorts) { + constructor(languageUseCase, intentUseCase, thinkUseCase, autofixUseCase, doUserRequestUseCase, actorAuthorizationPort, bugbotGitMutationPort, dismissBugbotFindingsUseCase, reviewPotentialProblemsUseCase, updatePullRequestDescriptionUseCase, rememberBugbotRuleUseCase, syncBranchUseCase, rememberBranchPorts, statusReadPorts, historyReadPort, sddDiffQueryPort, nativeStatusReadPort, doctorReadPorts) { this.languageUseCase = languageUseCase; this.intentUseCase = intentUseCase; this.thinkUseCase = thinkUseCase; @@ -55872,6 +56114,7 @@ class PullRequestReviewCommentUseCase { this.rememberBranchPorts = rememberBranchPorts; this.statusReadPorts = statusReadPorts; this.historyReadPort = historyReadPort; + this.sddDiffQueryPort = sddDiffQueryPort; this.nativeStatusReadPort = nativeStatusReadPort; this.doctorReadPorts = doctorReadPorts; this.taskId = "PullRequestReviewCommentUseCase"; @@ -55909,6 +56152,7 @@ class PullRequestReviewCommentUseCase { rememberBugbotRuleUseCase: this.rememberBugbotRuleUseCase, rememberBranchPorts: this.rememberBranchPorts, statusReadPorts: this.statusReadPorts, + sddDiffQueryPort: this.sddDiffQueryPort, doctorReadPorts: this.doctorReadPorts, nativeStatusReadPort: this.nativeStatusReadPort, syncBranchUseCase: this.syncBranchUseCase @@ -57231,6 +57475,191 @@ function canonicalIssueUrl(context) { } +/***/ }), + +/***/ 48684: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffQueryUseCase = void 0; +const issue_start_policy_1 = __nccwpck_require__(90332); +const issue_workflow_profile_1 = __nccwpck_require__(26744); +const pre_branch_sdd_diff_1 = __nccwpck_require__(28558); +/** Structural comparison over shared verified evidence, with no writer/model capability. */ +class SddDiffQueryUseCase { + constructor(ports) { + this.ports = Object.freeze({ + evidence: Object.freeze({ read: ports.evidence.read.bind(ports.evidence) }), + links: Object.freeze({ readClosingIssues: ports.links.readClosingIssues.bind(ports.links) }), + labels: Object.freeze({ getLabels: ports.labels.getLabels.bind(ports.labels) }), + descriptions: Object.freeze({ getDescription: ports.descriptions.getDescription.bind(ports.descriptions) }), + documents: Object.freeze({ readSdd: ports.documents.readSdd.bind(ports.documents) }), + }); + } + async read(request) { + try { + if (!positive(request.target.number) || !positive(request.repositoryId) + || !['issue', 'pull-request'].includes(request.target.kind)) + return { state: 'unavailable' }; + let links; + if (request.target.kind === 'pull-request') { + const association = await this.ports.links.readClosingIssues(request.target.number); + if (association.state !== 'verified' || association.pullNumber !== request.target.number + || association.issueNumbers.length > 100) + return { state: 'unavailable' }; + links = association; + } + const result = await this.readSnapshot(request, links); + // Every projection, including pending, absent and exempt, belongs to the + // association observed at admission. A moved association retains no old links. + if (links) { + const freshLinks = await this.ports.links.readClosingIssues(request.target.number); + if (freshLinks.state !== 'verified' || freshLinks.pullNumber !== request.target.number) + return { state: 'unavailable' }; + if (associationProof(freshLinks) !== associationProof(links)) + return { state: 'stale' }; + } + return Object.freeze(result); + } + catch { + return { state: 'unavailable' }; + } + } + async readSnapshot(request, links) { + let issueNumber = request.target.number; + let issueAuthor = request.issueAuthor; + if (links) { + if (links.issueNumbers.length === 0) + return { state: 'not-applicable' }; + const choice = await this.selectEligible(request, links); + if (choice.state !== 'selected') { + const freshChoice = await this.selectEligible(request, links); + return { state: freshChoice.state === choice.state ? choice.state : 'stale' }; + } + issueNumber = choice.context.issueNumber; + issueAuthor = choice.context.issueAuthor; + } + if (!issueAuthor?.trim()) + return { state: 'unavailable' }; + const context = await this.context(request, issueNumber, issueAuthor); + if (!context) + return { state: 'not-applicable' }; + const original = await this.ports.evidence.read(context); + const projection = projectEvidence(original, issueNumber); + if (original.state !== 'published' || !original.verified?.currentSha) + return projection; + if (!/^[a-f0-9]{64}$/u.test(original.authorityDigest ?? '')) + return { state: 'unavailable' }; + if (links && (links.headSha !== original.verified.branchHeadSha || links.headBranch !== original.verified.branchName)) { + return historicalStale(original, issueNumber); + } + const publication = publicationOf(original); + let result = { state: 'initial-only', publication }; + if (original.verified.currentSha !== original.verified.firstSha) { + const [before, after] = await Promise.all([ + this.ports.documents.readSdd(original.verified.firstSha, original.verified.path), + this.ports.documents.readSdd(original.verified.currentSha, original.verified.path), + ]); + if (before === undefined || after === undefined) + return { state: 'unavailable' }; + const changes = (0, pre_branch_sdd_diff_1.compareSddStructure)(before, after); + result = changes.state === 'bounded' + ? { state: 'bounded', publication, reason: changes.reason } + : { state: 'compared', publication, changes }; + } + // Re-admit the exact issue, then re-read the shared fenced authority. + let freshContext; + if (links) { + const choice = await this.selectEligible(request, links); + if (choice.state !== 'selected') + return { state: choice.state }; + if (choice.context.issueNumber !== issueNumber) + return historicalStale(original, issueNumber); + freshContext = choice.context; + } + else + freshContext = await this.context(request, issueNumber, issueAuthor); + if (!freshContext || freshContext.admittedKind !== context.admittedKind) + return { state: 'stale' }; + const fresh = await this.ports.evidence.read(freshContext); + if (!original.authorityDigest || fresh.authorityDigest !== original.authorityDigest + || proofOf(fresh) !== proofOf(original)) { + return fresh.state === 'published' ? historicalStale(fresh, issueNumber) : projectEvidence(fresh, issueNumber); + } + return result; + } + /** At most the adapter's fixed 100 native candidates; no body/number inference. */ + async selectEligible(request, links) { + let selected; + for (const number of links.issueNumbers) { + const author = links.issueAuthors[number]; + if (!author?.trim()) + throw new Error('Native issue author is unavailable.'); + const context = await this.context(request, number, author); + if (!context) + continue; + if (selected) + return { state: 'ambiguous' }; + selected = context; + } + return selected ? { state: 'selected', context: selected } : { state: 'not-applicable' }; + } + async context(request, issueNumber, issueAuthor) { + const [labels, body] = await Promise.all([ + this.ports.labels.getLabels(issueNumber), this.ports.descriptions.getDescription(issueNumber), + ]); + if (body === undefined) + throw new Error('Issue admission is unavailable.'); + const admission = (0, issue_workflow_profile_1.classifyIssueWorkflow)(labels, request.profile, request.labelConfiguration, body); + return admission.status === 'eligible' && (0, issue_start_policy_1.decideIssueStart)({ kind: admission.kind, labels, + issueManagedBranches: request.issueManagedBranches, preBranchSdd: request.preBranchSdd }).sddRequired ? Object.freeze({ issueNumber, admittedKind: admission.kind, issueAuthor, + profileDigest: request.profileDigest, trustedBotLogin: request.trustedBotLogin, + issueManagedBranches: request.issueManagedBranches, preBranchSdd: request.preBranchSdd, baseBranch: request.baseBranch }) : undefined; + } +} +exports.SddDiffQueryUseCase = SddDiffQueryUseCase; +function projectEvidence(evidence, issueNumber) { + if (evidence.state === 'not-required') + return { state: 'not-applicable' }; + if (evidence.state === 'absent') + return { state: 'no-publication' }; + if (evidence.state === 'invalid' || evidence.state === 'unavailable') + return { state: evidence.state }; + const publication = publicationOf(evidence); + if (evidence.state === 'stale') + return historicalStale(evidence, issueNumber); + if (evidence.state === 'awaiting-answers' || evidence.state === 'publication-pending') { + return publication ? { state: 'pending-revision', publication, ...cardOf(evidence, issueNumber) } : { state: 'no-publication' }; + } + if (!publication?.currentSha) + return { state: 'invalid' }; + return publication.currentSha === publication.firstSha ? { state: 'initial-only', publication } : { state: 'unavailable' }; +} +function publicationOf(evidence) { + return evidence.verified ? Object.freeze({ path: evidence.verified.path, firstSha: evidence.verified.firstSha, + ...(evidence.verified.currentSha ? { currentSha: evidence.verified.currentSha } : {}) }) : undefined; +} +function historicalStale(evidence, issueNumber) { + const publication = publicationOf(evidence); + return { state: 'stale', ...(publication ? { publication } : {}), ...cardOf(evidence, issueNumber) }; +} +function cardOf(evidence, issueNumber) { + return positive(evidence.cardId) ? { card: Object.freeze({ issueNumber, commentId: evidence.cardId }) } : {}; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function proofOf(evidence) { + const verified = evidence.verified; + return JSON.stringify([evidence.state, evidence.authorityDigest, evidence.cardId, evidence.path, evidence.pendingQuestionIds, + verified?.path, verified?.capabilityId, verified?.branchName, verified?.branchHeadSha, verified?.firstSha, verified?.currentSha]); +} +function associationProof(links) { + return JSON.stringify([links.pullNumber, links.headSha, links.headBranch, + links.issueNumbers.map(number => [number, links.issueAuthors[number].toLowerCase()])]); +} + + /***/ }), /***/ 39893: @@ -57240,6 +57669,7 @@ function canonicalIssueUrl(context) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SddEvidenceQueryUseCase = void 0; +const node_crypto_1 = __nccwpck_require__(6005); const issue_start_policy_1 = __nccwpck_require__(90332); const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_evidence_read_ports_1 = __nccwpck_require__(94752); @@ -57273,7 +57703,10 @@ class SddEvidenceQueryUseCase { if (!['invalid', 'unavailable'].includes(result.state) && !await this.fence(context, facts, selection.body, cardId, record, result.verified, historical)) return { state: 'stale' }; - return Object.freeze({ ...result, ...(historical ? { historical: true } : {}) }); + return Object.freeze({ ...result, ...(!['invalid', 'unavailable'].includes(result.state) ? { + authorityDigest: (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([cardId, selection.body, digest, canonicalLabels(facts.labels), + record.phase === 'awaiting-answer' ? facts.comments : null])).digest('hex'), + } : {}), ...(historical ? { historical: true } : {}) }); } catch { return { state: 'unavailable' }; @@ -63217,7 +63650,9 @@ async function runPublishResume(param, taskId, comments, catalogResolver, source (0, logging_ports_1.logInfo)('Conversation publication omitted: the configured bot identity is unavailable.'); return undefined; } - const catalog = await (0, publication_message_catalog_1.resolvePublicationCatalog)(param.locale, param.languageConfiguration, catalogResolver); + const catalog = statuses.length === 0 && replies.every(intent => intent.projection.kind === 'sdd-diff') + ? (0, publication_message_catalog_1.resolveStaticPublicationCatalog)(replies[0].locale).catalog + : await (0, publication_message_catalog_1.resolvePublicationCatalog)(param.locale, param.languageConfiguration, catalogResolver); let staleSourceEvidence; const compactedCommentIds = []; for (const intent of replies) { @@ -63349,7 +63784,7 @@ const publication_identity_policy_1 = __nccwpck_require__(45403); const publication_message_catalog_1 = __nccwpck_require__(34223); const semantic_result_publication_policy_1 = __nccwpck_require__(81985); const duplicate_comment_cleanup_workflow_1 = __nccwpck_require__(79544); -/** Publishes an explicit reply at most once for a trusted request correlation. */ +/** Publishes trusted correlated replies and refreshes verified read-only projections. */ async function reconcileReply(context, comments, requestRead) { if (!context.botLogin.trim()) return unchangedOutcome(); @@ -76880,6 +77315,85 @@ class RepositorySecretsCommandRepository { exports.RepositorySecretsCommandRepository = RepositorySecretsCommandRepository; +/***/ }), + +/***/ 85885: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddDiffLinkRepository = void 0; +const QUERY = `query SddDiffClosingIssues($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + databaseId nameWithOwner + pullRequest(number: $number) { + number headRefOid headRefName headRepository { databaseId nameWithOwner } repository { databaseId nameWithOwner } + closingIssuesReferences(first: 100) { + totalCount pageInfo { hasNextPage } + nodes { number author { login } repository { databaseId nameWithOwner } } + } + } + } +}`; +/** One bounded native read; absent, incomplete or conflicting identities never become guessed links. */ +class SddDiffLinkRepository { + constructor(client, binding) { + this.client = client; + this.binding = Object.freeze({ ...binding }); + } + async readClosingIssues(pullNumber) { + const unavailable = Object.freeze({ state: 'unavailable' }); + const binding = this.binding; + if (!positive(pullNumber) || !positive(binding.repositoryId) + || ![binding.owner, binding.repository].every(value => /^[A-Za-z0-9_.-]{1,100}$/u.test(value) && !['.', '..'].includes(value))) + return unavailable; + try { + const response = object(await this.client.getClient(binding.token).graphql(QUERY, { + owner: binding.owner, repo: binding.repository, number: pullNumber, + })); + const repository = object(response?.repository); + const pull = object(repository?.pullRequest); + const links = object(pull?.closingIssuesReferences); + const page = object(links?.pageInfo); + if (!sameRepository(repository, binding) || !sameRepository(object(pull?.repository), binding) + || !sameRepository(object(pull?.headRepository), binding) + || typeof pull?.headRefName !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,249}$/u.test(pull.headRefName) + || pull?.number !== pullNumber || typeof pull.headRefOid !== 'string' || !/^[a-f0-9]{40}$/u.test(pull.headRefOid) + || page?.hasNextPage !== false || !Array.isArray(links?.nodes) + || !Number.isSafeInteger(links.totalCount) || links.totalCount !== links.nodes.length || links.nodes.length > 100) + return unavailable; + const numbers = []; + const authors = {}; + for (const value of links.nodes) { + const issue = object(value); + const author = object(issue?.author); + if (typeof author?.login !== 'string' || !author.login.trim() || author.login.length > 100 + || !positive(issue?.number) || !sameRepository(object(issue?.repository), binding) + || numbers.includes(issue.number)) + return unavailable; + numbers.push(issue.number); + authors[issue.number] = author.login; + } + return Object.freeze({ state: 'verified', pullNumber, headSha: pull.headRefOid, headBranch: pull.headRefName, + issueNumbers: Object.freeze(numbers.sort((left, right) => left - right)), issueAuthors: Object.freeze(authors) }); + } + catch { + return unavailable; + } + } +} +exports.SddDiffLinkRepository = SddDiffLinkRepository; +function object(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined; +} +function positive(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value > 0; } +function sameRepository(value, binding) { + return value?.databaseId === binding.repositoryId && typeof value.nameWithOwner === 'string' + && value.nameWithOwner.toLowerCase() === `${binding.owner}/${binding.repository}`.toLowerCase(); +} + + /***/ }), /***/ 40941: @@ -77878,6 +78392,7 @@ exports.COPILOT_COMMAND_NAMES = [ 'estimate', 'test-plan', 'status', + 'sdd-diff', 'doctor', 'history', 'description', @@ -77941,6 +78456,8 @@ function parseCopilotCommand(raw) { && name !== 'remember') { return { kind: 'invalid', reason: `Copilot commands accept at most ${MAX_ARGUMENTS} arguments.` }; } + if (name === 'sdd-diff' && argumentsList.length > 0) + return { kind: 'invalid', reason: '/copilot sdd-diff does not accept arguments.' }; if (name === 'doctor' && argumentsList.length > 0) { return { kind: 'invalid', reason: '/copilot doctor does not accept arguments.' }; } @@ -80144,6 +80661,153 @@ function isRecord(value) { } +/***/ }), + +/***/ 28558: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SDD_DIFF_LIMITS = void 0; +exports.compareSddStructure = compareSddStructure; +exports.SDD_DIFF_LIMITS = Object.freeze({ + characters: 70000, bytes: 280000, lines: 2000, comparisons: 2000000, + sectionTitles: 8, titleCharacters: 120, +}); +class ComparisonBudgetExceeded extends Error { +} +/** Exact line counts with Hirschberg's linear-memory LCS; every equality consumes bounded work. */ +function compareSddStructure(before, after) { + for (const text of [before, after]) { + if (Buffer.byteLength(text, 'utf8') > exports.SDD_DIFF_LIMITS.bytes) + return Object.freeze({ state: 'bounded', reason: 'bytes' }); + if (text.length > exports.SDD_DIFF_LIMITS.characters) + return Object.freeze({ state: 'bounded', reason: 'characters' }); + } + const left = documentLines(before); + const right = documentLines(after); + if (left.length > exports.SDD_DIFF_LIMITS.lines || right.length > exports.SDD_DIFF_LIMITS.lines) { + return Object.freeze({ state: 'bounded', reason: 'lines' }); + } + const leftMatches = new Uint8Array(left.length); + const rightMatches = new Uint8Array(right.length); + let comparisons = 0; + let common = 0; + const equal = (a, b) => { + if (comparisons === exports.SDD_DIFF_LIMITS.comparisons) + throw new ComparisonBudgetExceeded(); + comparisons++; + return a === b; + }; + const match = (a, b) => { leftMatches[a] = 1; rightMatches[b] = 1; common++; }; + const row = (ls, le, rs, re, reverse) => { + let previous = new Uint16Array(re - rs + 1); + let current = new Uint16Array(re - rs + 1); + for (let i = 0; i < le - ls; i++) { + current[0] = 0; + for (let j = 1; j <= re - rs; j++) { + current[j] = equal(left[reverse ? le - i - 1 : ls + i], right[reverse ? re - j : rs + j - 1]) + ? previous[j - 1] + 1 : Math.max(previous[j], current[j - 1]); + } + [previous, current] = [current, previous]; + } + return previous; + }; + // Row arrays leave scope before recursion; no quadratic trace is retained. + const split = (ls, middle, le, rs, re) => { + const forward = row(ls, middle, rs, re, false); + const backward = row(middle, le, rs, re, true); + let best = -1; + let offset = 0; + for (let j = 0; j <= re - rs; j++) { + const score = forward[j] + backward[re - rs - j]; + if (score > best) { + best = score; + offset = j; + } + } + return rs + offset; + }; + const align = (ls, le, rs, re) => { + if (ls === le || rs === re) + return; + if (le - ls === 1) { + for (let j = rs; j < re; j++) + if (equal(left[ls], right[j])) { + match(ls, j); + break; + } + return; + } + const middle = ls + Math.floor((le - ls) / 2); + const rightMiddle = split(ls, middle, le, rs, re); + align(ls, middle, rs, rightMiddle); + align(middle, le, rightMiddle, re); + }; + try { + let start = 0; + let leftEnd = left.length; + let rightEnd = right.length; + while (start < leftEnd && start < rightEnd && equal(left[start], right[start])) { + match(start, start); + start++; + } + while (start < leftEnd && start < rightEnd && equal(left[leftEnd - 1], right[rightEnd - 1])) { + match(--leftEnd, --rightEnd); + } + align(start, leftEnd, start, rightEnd); + } + catch (error) { + if (!(error instanceof ComparisonBudgetExceeded)) + throw error; + return Object.freeze({ state: 'bounded', reason: 'work' }); + } + const titles = new Set(); + collectChangedSections(right, rightMatches, titles); + collectChangedSections(left, leftMatches, titles); + return Object.freeze({ state: 'compared', addedLines: right.length - common, removedLines: left.length - common, + sections: Object.freeze([...titles].slice(0, exports.SDD_DIFF_LIMITS.sectionTitles).map(boundedTitle)), + omittedSections: Math.max(0, titles.size - exports.SDD_DIFF_LIMITS.sectionTitles) }); +} +/** CRLF and terminal newline markers do not count as added/removed content lines. */ +function documentLines(text) { + if (!text) + return []; + const lines = text.split(/\r?\n/u); + if (text.endsWith('\n')) + lines.pop(); + return lines; +} +function collectChangedSections(lines, matches, titles) { + let fence; + let title; + for (let index = 0; index < lines.length; index++) { + const boundary = lines[index].match(/^ {0,3}(`{3,}|~{3,})(.*)$/u); + if (boundary) { + if (!fence && (boundary[1][0] !== '`' || !boundary[2].includes('`'))) + fence = boundary[1]; + else if (fence && boundary[1][0] === fence[0] && boundary[1].length >= fence.length + && /^[ \t]*$/u.test(boundary[2])) + fence = undefined; + } + else if (!fence) { + const heading = lines[index].match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/u); + if (heading) + title = heading[1]; + } + if (matches[index] === 0 && title) + titles.add(title); + } +} +function boundedTitle(title) { + let value = title.replace(/[\p{Cc}\p{Cf}\u2028\u2029]/gu, ' ').slice(0, exports.SDD_DIFF_LIMITS.titleCharacters); + if (/[\uD800-\uDBFF]$/u.test(value)) + value = value.slice(0, -1); + return value; +} + + /***/ }), /***/ 8024: @@ -83407,6 +84071,7 @@ exports.createPullRequestReviewCommentUseCaseCompositionRoot = createPullRequest exports.createCommitUseCaseCompositionRoot = createCommitUseCaseCompositionRoot; exports.createMainRunRouteCompositionRoot = createMainRunRouteCompositionRoot; exports.createClosedIssueRetirementUseCase = createClosedIssueRetirementUseCase; +const sdd_diff_composition_root_1 = __nccwpck_require__(83434); const pull_request_status_composition_root_1 = __nccwpck_require__(49891); const github_runtime_doctor_composition_root_1 = __nccwpck_require__(36459); const reply_request_composition_root_1 = __nccwpck_require__(99065); @@ -83533,7 +84198,7 @@ function createIssueCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, (0, issue_use_case_composition_root_1.createIssueUseCaseCompositionRoot)(binding), rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_evidence_composition_root_1.createSddEvidenceQueryPort)(binding, bugbot.scm.context), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); } function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { const bugbot = (0, bugbot_composition_root_1.createBugbotCompositionRoot)(binding); @@ -83549,7 +84214,7 @@ function createPullRequestReviewCommentUseCaseCompositionRoot(binding) { contextPorts: bugbot.scm.context, resolutionPorts: bugbot.scm.resolution, catalogResolver: new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language), - }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); + }), new detect_potential_problems_use_case_1.DetectPotentialProblemsUseCase(findings, bugbot.scm, bugbot.telemetry, new resolve_message_catalog_use_case_1.ResolveMessageCatalogUseCase(language)), pullRequestDescription, new remember_bugbot_rule_use_case_1.RememberBugbotRuleUseCase(bugbot.rules), branchSync, rememberBranchPorts(binding, bugbot.scm.context), (0, pull_request_status_composition_root_1.createPullRequestStatusReadPorts)(binding, bugbot.scm.context), (0, copilot_history_port_factory_1.createCopilotHistoryReadPort)(bugbot.scm.context, process.env.GITHUB_SERVER_URL ?? 'https://github.com'), (0, sdd_diff_composition_root_1.createSddDiffQueryPort)(binding, bugbot.scm.context), (0, issue_completion_composition_root_1.createNativeStatusReadPort)(binding), (0, github_runtime_doctor_composition_root_1.createGithubRuntimeDoctorPorts)(binding, bugbot.scm.context)); } function createCommitUseCaseCompositionRoot(projectBoardCommandPort, binding) { return new commit_use_case_1.CommitUseCase(new notify_new_commit_on_issue_use_case_1.NotifyNewCommitOnIssueUseCase((0, push_single_action_capability_port_binding_1.bindIssueReopen)((0, issue_interaction_composition_root_1.createIssueNotificationRepository)(), binding)), new check_changes_issue_size_use_case_1.CheckChangesIssueSizeUseCase((0, lifecycle_capability_port_binding_2.bindProjectBoardCommands)(projectBoardCommandPort, binding), (0, lifecycle_capability_port_binding_2.bindIssueLabels)((0, issue_labels_composition_root_1.createIssueLabelRepository)(), binding), (0, push_single_action_capability_port_binding_1.bindPullRequestBranchQuery)(new pull_request_lifecycle_repository_1.PullRequestLifecycleRepository((0, github_pull_request_client_factory_1.createPullRequestLifecycleClient)()), binding), (0, push_single_action_capability_port_binding_1.bindBranchChangeSize)(new branch_compare_repository_1.BranchCompareRepository((0, github_branch_client_factory_1.createBranchComparisonClient)()), binding)), createDetectPotentialProblemsUseCase(binding), (0, check_progress_composition_root_1.createCheckProgressCompositionRoot)(binding), (0, actor_authorization_composition_root_1.createActorAuthorizationRepository)()); @@ -84052,6 +84717,38 @@ function createReplyRequestReadPort(binding) { } +/***/ }), + +/***/ 83434: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.createSddDiffQueryPort = createSddDiffQueryPort; +const sdd_evidence_query_use_case_1 = __nccwpck_require__(39893); +const sdd_diff_query_use_case_1 = __nccwpck_require__(48684); +const sdd_diff_link_repository_1 = __nccwpck_require__(85885); +const pre_branch_sdd_workspace_adapter_1 = __nccwpck_require__(35849); +const sdd_evidence_composition_root_1 = __nccwpck_require__(51671); +const github_project_client_factory_1 = __nccwpck_require__(23691); +/** No agent, provisioning, approval or lifecycle writer is supplied. */ +function createSddDiffQueryPort(binding, comments) { + const bound = Object.freeze({ ...binding }); + const reads = (0, sdd_evidence_composition_root_1.createSddEvidenceReadPorts)(bound, comments); + const evidence = new sdd_evidence_query_use_case_1.SddEvidenceQueryUseCase(reads); + const documents = new pre_branch_sdd_workspace_adapter_1.PreBranchSddWorkspaceAdapter(process.cwd(), bound.token); + const client = (0, github_project_client_factory_1.createGraphqlTransportClient)(); + return Object.freeze({ serverOrigin: process.env.GITHUB_SERVER_URL ?? 'https://github.com', + read: (request) => new sdd_diff_query_use_case_1.SddDiffQueryUseCase({ evidence: Object.freeze({ read: evidence.read.bind(evidence) }), + links: new sdd_diff_link_repository_1.SddDiffLinkRepository(client, { ...bound, repositoryId: request.repositoryId }), + labels: reads.labels, descriptions: reads.descriptions, + documents: Object.freeze({ readSdd: documents.readSdd.bind(documents) }), + }).read(request), + }); +} + + /***/ }), /***/ 51671: diff --git a/docs/issues/comment-commands.mdx b/docs/issues/comment-commands.mdx index 2fddfd244..398edad82 100644 --- a/docs/issues/comment-commands.mdx +++ b/docs/issues/comment-commands.mdx @@ -44,8 +44,10 @@ Commands are recognized at the beginning of a comment, case-insensitively. The c | `/copilot review` | Run the read-only review flow. | No | | `/copilot findings` | Reconcile and publish current findings. | No | | `/copilot recheck` | Re-run the review against the current branch. | No | +| `/copilot sdd-diff` | Compare verified first/current SDD publications without an agent. | No | | `/copilot status` | Show the current Copilot status. | No | | `/copilot doctor` | Check runtime prerequisites using bounded read-only evidence. | No | +| `/copilot history` | Show supported requests and verified receipts in the main conversation. | No | | `/copilot description` | Refresh the PR description when that action is configured. | No | | `/copilot fix [finding-id ...]` | Fix one or more existing Bugbot findings. | Yes, if authorized | | `/copilot dismiss [finding-id ...]` | Dismiss one or more findings. | No file changes | @@ -61,8 +63,8 @@ whitespace-separated, and commands are limited to 2,000 characters and 20 arguments. Missing or malformed required arguments produce a validation reply; they never mean “all” or authorize a broad change. -`help`, `status`, `doctor`, `dismiss`, `remember` and rejected commands do not run a -model or change the temporary agent activity label. `sync-branch --dry-run` +`help`, `status`, `sdd-diff`, `doctor`, `history`, `dismiss`, `remember` and +rejected commands do not run a model or change the temporary agent activity label. `sync-branch --dry-run` and `--no-agent` also avoid model activity. Ordinary sync may use the conflict fixer, and `findings` runs a fresh review. These distinctions preserve each command's existing authorization and repository effects. @@ -427,6 +429,34 @@ SDD status keeps an unknown requirement visible when the issue has no admitted workflow kind. Evidence links use the configured GitHub server origin and the same issue identity; an untrusted or mismatched URL is shown without navigation. +## Compare SDD publications + +Run `/copilot sdd-diff` without arguments in an eligible issue. The reply links +verified immutable first/current SDD publications and their commits, and compares +added/removed lines and up to eight changed section titles. The commit comparison +link can include other files changed between those commits; the reported counts +cover only the owning SDD. This is a structural comparison, without an assessment +of semantic requirements or document excerpts. + +A first publication without a revision reports `initial-only`. Pending answers +and a changed issue withhold current counts and link the existing clarification +card when verified. Missing, invalid or unavailable evidence is explicit. The +command reads current issue, record and branch facts again before claiming a +current comparison. It does not run an agent, change activity labels or edit files. + +On a PR, exactly one SDD-eligible issue among the native same-repository closing +links is required, and its current linked branch must match the PR head. Copilot +checks at most 100 native candidates and reports ambiguity for multiple eligible +issues; the command never guesses from issue numbers in text. Use the intended +issue conversation to resolve ambiguity. The same eligibility selection is rechecked before returning counts. A replay of the same source comment +refreshes its existing owned reply; a new comment gets a separate reply. + +Comparison limits are fixed: 70,000 characters, 280,000 UTF-8 bytes and 2,000 +content lines per document, and 2,000,000 line-comparison steps. Over-limit +results retain safe verified links and withhold exact counts. English and Spanish +are bundled; other locales receive a complete English fallback without a model. +Paths, refs, URLs and configurable limits are not accepted as command arguments. + Doctor treats a valid native issue with no description as an empty body, so independent checks can still complete. Unknown evidence groups are rejected; installed workflow activity filters must contain a non-empty array of text @@ -440,6 +470,13 @@ When the detected request language and writing script already match the target, Copilot keeps the original wording and omits a redundant translation disclosure. Mixed-language requests and different writing scripts can still be translated. + +On a PR, `/copilot sdd-diff` verifies the native closing-issue association again +before returning every result, including pending or missing publication. If the +association changes, retry the command: the stale reply withholds links belonging +to the previous issue. An unreadable final association remains unavailable. + +An exempt or ambiguous PR comparison rechecks linked issue eligibility before returning. Stale and pending replies preserve both verified publications as explicitly historical when their commits differ; they keep current comparison links and change counts unavailable. Check-mode PR readiness reports the configured coverage check independently of other CI checks. Coverage can be satisfied while another CI check is blocked, or pending when its current revision is missing or still running. Overall @@ -541,6 +578,9 @@ source check. If the event lacks that request evidence, the Action withholds creation, updates and duplicate cleanup instead of assuming freshness. Initial help answers from non-comment events retain their ordinary immutable reply. +SDD comparison replies require the exact source comment witness before they are +created, refreshed or deduplicated. If that proof is missing or changes, the +existing reply remains unchanged; use a valid new query after checking the source. `/copilot history` in a pull request's main conversation reads and publishes for that exact PR, even when its linked issue has a different number. The query uses @@ -583,3 +623,9 @@ Doctor distinguishes its source links: **Inspected issue** opens the issue conte opens the immutable repository tree that was inspected. Spanish uses the matching entity captions. These links do not authorize a repair or certify CI, producer health or write permissions. + + +Comment-triggered replies require a verified native source comment before +publication, including repeated read-only queries and immutable operation +receipts. If the source witness or its reader is unavailable, Copilot keeps +existing replies and performs no comment creation, update or duplicate cleanup. diff --git a/docs/issues/pre-branch-sdds.mdx b/docs/issues/pre-branch-sdds.mdx index 283ce1f65..34094d4d5 100644 --- a/docs/issues/pre-branch-sdds.mdx +++ b/docs/issues/pre-branch-sdds.mdx @@ -95,3 +95,20 @@ unavailable; a corrupted card, changed issue or racing branch cannot produce a ready result. A newer invalid owned card never restores an older ready state. Answers from older rounds without matching question provenance remain pending. Help issues remain branchless and do not require an SDD. + + +## Compare the first and current publication + +Use `/copilot sdd-diff` in the issue to inspect a bounded structural comparison +of the first and current verified SDD. A reply shows immutable document and +commit links, exact added/removed line counts and up to eight escaped changed +section titles. It does not quote document contents or decide whether semantic +requirements changed. The commit comparison may show other files; the command's +counts concern only the owning SDD. + +Only a first publication means there is no revision pair. Pending clarifications +or stale issue evidence withhold current counts and return you to the existing +SDD card. Invalid or unavailable authority cannot certify a comparison. Repeating +the same source comment refreshes one reply; it never regenerates the SDD or +changes the branch. See [comment commands](./comment-commands.mdx) for exact PR +link rules, fixed limits and the English/Spanish fallback contract. diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index fe0e31392..563fbc6b4 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -564,6 +564,31 @@ ], "successMessage": "canonical PR status coverage: PASS (marker policy 100%; read workflow 95% lines/statements, 90% branches/functions)" }, + { + "name": "SDD structural comparison", + "missingEntryLabel": "SDD structural comparison", + "rules": [ + { + "files": [ + "src/domain/pre_branch_sdd_diff.ts", + "src/application/policies/sdd_diff_message_catalog.ts", + "src/application/policies/sdd_diff_presentation_policy.ts" + ], + "mode": "each", + "thresholdProfile": "exhaustive" + }, + { + "files": [ + "src/application/usecases/sdd/sdd_diff_query_use_case.ts", + "src/data/repository/sdd/sdd_diff_link_repository.ts", + "src/infrastructure/composition/sdd_diff_composition_root.ts" + ], + "mode": "each", + "thresholdProfile": "default" + } + ], + "successMessage": "SDD structural comparison coverage: PASS (pure policies 100%; each application/adapter/composition 95% lines/statements, 90% branches/functions)" + }, { "name": "Git hook verification environment", "missingEntryLabel": "Git hook verification environment", diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 061b1c9c8..d53d3711e 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -20,7 +20,7 @@ debt or convert unknown historic intent into a design decision. | `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 1 companion | 202 paths · 2026-10-13 | | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 34 paths · 2026-10-07 | -| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 2 companion | 84 paths · 2026-10-12 | +| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 104 paths · 2026-10-13 | | `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) | 56 paths · 2026-10-13 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 108 paths · 2026-10-13 | @@ -147,12 +147,12 @@ debt or convert unknown historic intent into a design decision. ### `issue-start-and-sdd-readiness` — Uniform issue start and pre-branch SDD readiness - Owner: Copilot maintainers -- Last verified: 2026-10-12 -- Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) · [`specs/pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.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_commit.yml`](../.github/workflows/copilot_commit.yml) +- Last verified: 2026-10-13 +- Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) · [`specs/pre-branch-sdd-diff.md`](./pre-branch-sdd-diff.md) · [`specs/pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.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_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`action.yml`](../action.yml) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) -- Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/domain/agent_response_length_failure.ts`](../src/domain/agent_response_length_failure.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/application/ports/sdd_evidence_read_ports.ts`](../src/application/ports/sdd_evidence_read_ports.ts) · [`src/application/policies/sdd_issue_identity_policy.ts`](../src/application/policies/sdd_issue_identity_policy.ts) · [`src/application/policies/sdd_query_card_policy.ts`](../src/application/policies/sdd_query_card_policy.ts) · [`src/application/policies/sdd_status_policy.ts`](../src/application/policies/sdd_status_policy.ts) · [`src/application/usecases/sdd/sdd_evidence_query_use_case.ts`](../src/application/usecases/sdd/sdd_evidence_query_use_case.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/comment_automation_contracts.ts`](../src/application/usecases/comment_automation_contracts.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/infrastructure/composition/sdd_evidence_composition_root.ts`](../src/infrastructure/composition/sdd_evidence_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) -- Tests: [`src/domain/__tests__/issue_workflow_runtime_policy.test.ts`](../src/domain/__tests__/issue_workflow_runtime_policy.test.ts) · [`src/data/model/__tests__/execution.test.ts`](../src/data/model/__tests__/execution.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.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/tooling/__tests__/validate_specification_catalog.test.ts`](../src/tooling/__tests__/validate_specification_catalog.test.ts) · [`src/domain/__tests__/issue_start_policy.test.ts`](../src/domain/__tests__/issue_start_policy.test.ts) · [`src/domain/__tests__/pre_branch_sdd.test.ts`](../src/domain/__tests__/pre_branch_sdd.test.ts) · [`src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts) · [`src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts`](../src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/actions/__tests__/input_boolean_policy.test.ts`](../src/actions/__tests__/input_boolean_policy.test.ts) · [`src/domain/__tests__/issue_workflow_profile.test.ts`](../src/domain/__tests__/issue_workflow_profile.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/policies/__tests__/sdd_issue_identity_policy.test.ts`](../src/application/policies/__tests__/sdd_issue_identity_policy.test.ts) · [`src/application/policies/__tests__/sdd_query_card_policy.test.ts`](../src/application/policies/__tests__/sdd_query_card_policy.test.ts) · [`src/application/policies/__tests__/sdd_status_policy.test.ts`](../src/application/policies/__tests__/sdd_status_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts) · [`src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts`](../src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.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__/sdd_evidence_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts) · [`src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts`](../src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts) +- Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/domain/agent_response_length_failure.ts`](../src/domain/agent_response_length_failure.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/application/ports/sdd_evidence_read_ports.ts`](../src/application/ports/sdd_evidence_read_ports.ts) · [`src/application/policies/sdd_issue_identity_policy.ts`](../src/application/policies/sdd_issue_identity_policy.ts) · [`src/application/policies/sdd_query_card_policy.ts`](../src/application/policies/sdd_query_card_policy.ts) · [`src/application/policies/sdd_status_policy.ts`](../src/application/policies/sdd_status_policy.ts) · [`src/application/usecases/sdd/sdd_evidence_query_use_case.ts`](../src/application/usecases/sdd/sdd_evidence_query_use_case.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/comment_automation_contracts.ts`](../src/application/usecases/comment_automation_contracts.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/infrastructure/composition/sdd_evidence_composition_root.ts`](../src/infrastructure/composition/sdd_evidence_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/domain/pre_branch_sdd_diff.ts`](../src/domain/pre_branch_sdd_diff.ts) · [`src/application/policies/sdd_diff_message_catalog.ts`](../src/application/policies/sdd_diff_message_catalog.ts) · [`src/application/policies/sdd_diff_presentation_policy.ts`](../src/application/policies/sdd_diff_presentation_policy.ts) · [`src/application/ports/sdd_diff_read_ports.ts`](../src/application/ports/sdd_diff_read_ports.ts) · [`src/application/usecases/sdd/sdd_diff_query_use_case.ts`](../src/application/usecases/sdd/sdd_diff_query_use_case.ts) · [`src/data/repository/sdd/sdd_diff_link_repository.ts`](../src/data/repository/sdd/sdd_diff_link_repository.ts) · [`src/infrastructure/composition/sdd_diff_composition_root.ts`](../src/infrastructure/composition/sdd_diff_composition_root.ts) · [`src/application/usecases/pull_request_review_comment_use_case.ts`](../src/application/usecases/pull_request_review_comment_use_case.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) +- Tests: [`src/domain/__tests__/issue_workflow_runtime_policy.test.ts`](../src/domain/__tests__/issue_workflow_runtime_policy.test.ts) · [`src/data/model/__tests__/execution.test.ts`](../src/data/model/__tests__/execution.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.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/tooling/__tests__/validate_specification_catalog.test.ts`](../src/tooling/__tests__/validate_specification_catalog.test.ts) · [`src/domain/__tests__/issue_start_policy.test.ts`](../src/domain/__tests__/issue_start_policy.test.ts) · [`src/domain/__tests__/pre_branch_sdd.test.ts`](../src/domain/__tests__/pre_branch_sdd.test.ts) · [`src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts) · [`src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts`](../src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/actions/__tests__/input_boolean_policy.test.ts`](../src/actions/__tests__/input_boolean_policy.test.ts) · [`src/domain/__tests__/issue_workflow_profile.test.ts`](../src/domain/__tests__/issue_workflow_profile.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/policies/__tests__/sdd_issue_identity_policy.test.ts`](../src/application/policies/__tests__/sdd_issue_identity_policy.test.ts) · [`src/application/policies/__tests__/sdd_query_card_policy.test.ts`](../src/application/policies/__tests__/sdd_query_card_policy.test.ts) · [`src/application/policies/__tests__/sdd_status_policy.test.ts`](../src/application/policies/__tests__/sdd_status_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts) · [`src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts`](../src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.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__/sdd_evidence_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts) · [`src/domain/__tests__/pre_branch_sdd_diff.test.ts`](../src/domain/__tests__/pre_branch_sdd_diff.test.ts) · [`src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts`](../src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts) · [`src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts`](../src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts) · [`src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts) · [`src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts`](../src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts) · [`src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts`](../src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts) · [`src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts`](../src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts) - User documentation: [`docs/issues/branch-management.mdx`](../docs/issues/branch-management.mdx) · [`docs/issues/workflow-setup.mdx`](../docs/issues/workflow-setup.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/development/specifications.mdx`](../docs/development/specifications.mdx) · [`docs/issues/pre-branch-sdds.mdx`](../docs/issues/pre-branch-sdds.mdx) · [`docs/issues/index.mdx`](../docs/issues/index.mdx) · [`docs/issues/labels-and-branch-types.mdx`](../docs/issues/labels-and-branch-types.mdx) · [`docs/issues/configuration.mdx`](../docs/issues/configuration.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) ### `managed-issue-lifecycle` — Managed issue and branch lifecycle diff --git a/specs/catalog.json b/specs/catalog.json index c91664f7a..28849feb2 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1244,18 +1244,20 @@ "id": "issue-start-and-sdd-readiness", "title": "Uniform issue start and pre-branch SDD readiness", "status": "implemented", - "scope": "Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work", + "scope": "Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons", "owner": "Copilot maintainers", - "lastVerified": "2026-10-12", + "lastVerified": "2026-10-13", "specs": [ "specs/issue-start-and-branch-readiness.md", "specs/pre-branch-sdd-gate.md", + "specs/pre-branch-sdd-diff.md", "specs/pre-branch-sdd-status-readiness.md" ], "workflows": [ ".github/workflows/copilot_issue.yml", ".github/workflows/copilot_issue_comment.yml", - ".github/workflows/copilot_commit.yml" + ".github/workflows/copilot_commit.yml", + ".github/workflows/copilot_pull_request_comment.yml" ], "entrypoints": [ "action.yml", @@ -1302,7 +1304,19 @@ "src/application/usecases/issue_comment_use_case.ts", "src/application/policies/semantic_result_publication_policy.ts", "src/infrastructure/composition/sdd_evidence_composition_root.ts", - "src/infrastructure/composition/main_run_route_composition_root.ts" + "src/infrastructure/composition/main_run_route_composition_root.ts", + "src/domain/pre_branch_sdd_diff.ts", + "src/application/policies/sdd_diff_message_catalog.ts", + "src/application/policies/sdd_diff_presentation_policy.ts", + "src/application/ports/sdd_diff_read_ports.ts", + "src/application/usecases/sdd/sdd_diff_query_use_case.ts", + "src/data/repository/sdd/sdd_diff_link_repository.ts", + "src/infrastructure/composition/sdd_diff_composition_root.ts", + "src/application/usecases/pull_request_review_comment_use_case.ts", + "src/application/usecases/steps/common/reply_publication_workflow.ts", + "src/application/usecases/steps/common/publish_resume_workflow.ts", + "scripts/coverage-budgets.json", + "src/architecture/github_publication_mutation_baseline.json" ], "tests": [ "src/domain/__tests__/issue_workflow_runtime_policy.test.ts", @@ -1332,6 +1346,13 @@ "src/application/usecases/__tests__/read_copilot_status_workflow.test.ts", "src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts", "src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts", + "src/domain/__tests__/pre_branch_sdd_diff.test.ts", + "src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts", + "src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts", + "src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts", + "src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts", + "src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts", + "src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts", "src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts" ], "documentation": [ diff --git a/specs/pre-branch-sdd-diff.md b/specs/pre-branch-sdd-diff.md new file mode 100644 index 000000000..3602001c3 --- /dev/null +++ b/specs/pre-branch-sdd-diff.md @@ -0,0 +1,494 @@ +# Proposed change appendix: `/copilot sdd-diff` + +- Status: Ready +- Date: 2026-10-12 +- Catalog capability ID: `issue-start-and-sdd-readiness` +- Last verified: 2026-10-12 (local integration; live acceptance pending) +- Owners: Existing `issue-start-and-sdd-readiness` capability owners; individual names are not established by the issue. +- Scope: Add a deterministic, read-only command that compares the immutable first publication and current verified publication of the owning SDD for an eligible issue. +- Related issues/PRs: [Issue #464](https://github.com/vypdev/copilot/issues/464); [issue #454](https://github.com/vypdev/copilot/issues/454) provides the first/revised SDD human UX fixture. +- Required review gates: product UX, architecture, testing, documentation, security/operations +- Open decisions blocking readiness: None. PR #465 is merged and the Action synchronized this exact managed branch to the shared foundation. Local implementation is verified; fresh PR review and live issue/PR acceptance remain separate completion gates. + +> This is a prospective companion appendix. Preserve the owning SDD and its existing contract verbatim; append this proposed change. Repository-specific paths and contracts cited below must be reconciled with the existing capability before implementation. + +## 1. Executive summary + +Add `/copilot sdd-diff` with no arguments. Any GitHub participant permitted to comment may invoke it as a deterministic query, subject to the existing issue-start admission/profile policy. For an eligible issue, the command verifies the trusted gate record, exact admitted issue, owning catalogued SDD path, exact linked branch, immutable publications and current issue digest. It then compares the first and current published documents and presents bounded structural evidence with safe immutable links. It does not infer semantic requirement changes. + +A ready comparison is permitted only when the digest matches, the current publication is verified and the final freshness fence succeeds. Stale, clarification-pending, initial-only, invalid, absent and unavailable evidence remain distinct. The command has no model or write capability. + +```text +comment -> parse exact no-argument command -> resolve eligible target and trusted evidence + -> verify digest, branch, publications and ancestry -> bounded immutable diff + -> freshness fence -> update one correlated localized reply +``` + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem + +Developers can inspect an SDD gate's first publication and later revisions, but cannot ask the conversation for a bounded structural comparison. Showing an unverified or pending revision as current requirements could mislead branch work. + +### 2.2 Current behavior + +1. The existing gate records first publication and subsequent revision information; the issue states that first-SDD commit preservation already exists. +2. No current `/copilot sdd-diff` behavior is asserted by the issue. +3. The proposed command uses existing admission, gate record, catalog ownership, linked-branch readiness and publication verification contracts; it introduces no new record store. + +### 2.3 Evidence + +- Code/workflow/documentation references: supplied answers identify `readSddGateRecord` schema v1, `PreBranchSddWorkspacePort.readSdd`, `verifyPublication`, `isAncestor`, `BoundLinkedBranchReadinessPort.getLinkedBranch`, command parsing/help and comment orchestration. The confirmed semantic boundary is `SddEvidenceQueryPort` in `src/application/ports/sdd_evidence_read_ports.ts` from PR #465. The existing immutable content read is `PreBranchSddWorkspacePort.readSdd` in `src/application/ports/pre_branch_sdd_ports.ts`; the command may receive only a read-only projection of it. +- Incidents or concrete examples: issue #464 reports that developers cannot inspect bounded changes; issue #454 is named as a real first/revised SDD human UX fixture. +- External primary sources: not applicable; the contract is repository-owned. +- Remaining evidence: combined application/composition/routes against the Action-integrated shared query, fresh independent PR review, and live command/replay/locale acceptance. Operator GraphQL reads prove the native response shape, not bot-token permissions or a running command. + +### 2.4 Retrospective classification (as-built baselines only) + +Not applicable: this is a prospective specification, not a reconstruction of implemented behavior. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| Issue participant | Inspect published SDD structure | Issue comment `/copilot sdd-diff` | One correlated issue reply and immutable links | +| PR participant | Query only an explicitly linked eligible issue | PR comment | One correlated PR reply, or not-applicable/ambiguous/unavailable result | +| Maintainer | Preserve trust and operability | Catalog, docs, checks | Capability evidence and validation output | + +**First publication** is the immutable `commitSha`/`baseSha` pair. **Current publication** is `revisionSha`/`revisionBaseSha` when present. **Structural diff** means added/removed lines and changed section titles; it makes no claim about semantic requirements. **Stale** means the current normalized issue digest differs from the trusted record, or exact issue/card/branch/PR authority changed or disagreed during the final fence. **Pending-revision** means the digest matches an awaiting-answer record while the verified first publication remains available; the requested revision is not yet published. + +## 4. Goals + +### 4.1 Goals + +1. Provide a deterministic, read-only comparison for eligible targets. +2. Identify exact immutable first/current SHAs and verified owning SDD path. +3. Keep state distinctions honest and output bounded under hostile or excessive input. +4. Reuse existing gate ownership and correlated-comment behavior without changing the gate contract. + +### 4.2 Non-goals + +1. Semantic analysis, model calls, requirement inference, or content excerpts. +2. Editing source or SDDs, creating branches, changing approvals, labels, checks, or lifecycle state. +3. Arbitrary paths/refs, branch scans, PR-number equivalence, or user-provided URLs. + +### 4.3 Fixed product/safety invariants + +1. No current-comparison claim or counts without exact identity, valid trusted record, matching digest, verified publications/ancestry, exact linked branch and successful final freshness fence. +2. No writer capability; no model; no source content in replies. +3. Files and computation are bounded: per document at most 70,000 UTF-16 characters, 280,000 UTF-8 bytes and 2,000 lines; at most 2,000,000 line-comparison steps. The complete reply remains at most 6,000 characters; the renderer reserves wrapper space by limiting its body to 5,600 characters. When both document byte and character ceilings are exceeded, the byte ceiling is reported first; neither limit is raised. +4. At most eight changed section titles, each at most 120 characters, with omitted-title count disclosed. These fixed limits cannot be configured. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | User/operator effect | +|---|---|---|---| +| Issue query | No diff command asserted | Parse exact no-argument command | Predictable entry | +| Target proof | Existing gate policy | Exact issue admission; PR native closing-issue relationship only | No guessed target | +| Evidence | Gate retains publication records | Verify record, digest, linked branch, immutable publication and ancestry | Trustworthy state | +| Comparison | No conversation comparison asserted | Bounded line diff and section titles | Structural changes visible | +| Reply | Existing orchestration | Localized, replay-safe single correlated reply | No duplicate noise | + +```mermaid +flowchart LR + A[Comment] --> B[Resolve exact eligible issue] + B --> C[Verify record, digest, branch, publications] + C --> D[Bounded structural diff] + D --> E[Freshness fence] + E --> F[One localized correlated reply] +``` + +Text equivalent: comment -> exact target and evidence verification -> bounded comparison -> freshness check -> correlated reply. + +## 6. Functional behavior and state model + +### 6.1 Happy path + +1. Parse `/copilot sdd-diff` with no arguments using the existing deterministic parser. Invalid arguments follow existing help behavior and do not invoke a model. +2. On an issue, apply existing admission/profile policy: feature issues and admitted branch-bearing contract-change issues for which the SDD gate applies are eligible. Unsupported, exempt and help targets return not-applicable without writes other than the correlated query reply. +3. On a PR, query the exact event PR's native GitHub `closingIssuesReferences` relationship through a narrow read-only projection. Validate same-repository canonical PR identity and pagination completeness. Read admission and the existing SDD/start policy for each of at most 100 native closing issues. Select exactly one SDD-eligible issue; zero eligible issues is not-applicable and multiple eligible issues are ambiguous. An unreadable candidate is unavailable rather than assumed ineligible. Recheck selection after document reads and retain the final native association fence. Incomplete/unreadable authority is unavailable. Never infer from equal numbers, body text, branch scans, or a newest candidate. +4. Read the latest configured-bot-owned valid schema-v1 gate record for the exact issue. Confirm current catalog ownership and its allowlisted SDD path. Preserve `commitSha`/`baseSha` as first publication; use `revisionSha`/`revisionBaseSha` as current publication when present. Do not weaken schema or create another store. +5. Verify exact branch linkage and live branch name/head using existing readiness proof. Verify publication and ancestry at immutable SHAs. A missing, deleted, mismatched or unverifiable branch/publication cannot support a current comparison. +6. Compute the same digest as the gate from current normalized title, visible human body, admitted kind, profile and original recorded base. Digest mismatch takes precedence and reports stale. A digest-matching awaiting-answer record with verified first publication reports pending-revision. Neither permits current counts. A current comparison requires matching digest and verified current publication. +7. Read only the catalogued SDD at the two verified immutable SHAs. Apply all fixed input and work limits. If exceeded, report bounded/unavailable with safe verified links and a specific limit explanation; never truncate and claim exact counts. +8. Recheck issue title/body, record, branch identity and relevant publication facts before replying. Any race invalidates current claims. +9. Use effective issue locale, or PR locale on the PR surface. Render complete English/Spanish catalogs; unsupported locale or invalid catalog uses atomic English fallback. Update/reuse the existing exact repository/target/source-comment correlated reply. Replays update one reply; a new source comment gets its own. + +### 6.2 Alternative paths + +- Eligible issue before first publication: `no-published-SDD`. +- Valid initial publication without revision: `initial-only`; no two-revision comparison. +- Pending clarification with matching digest and verified first publication: `pending-revision`; no current counts. +- Digest mismatch: `stale`, with SDD question-card link; not pending-revision. +- Forged, foreign, malformed or unknown-version record: invalid/unavailable, never a comparison. +- Missing branch/publication and provider read errors: distinct missing/unavailable states where evidence permits; do not fabricate facts. +- PR with zero native closing issues or an ineligible sole issue: not-applicable; ambiguity and authority failure are explicit. +- Changed documents that exceed limits: bounded/unavailable; safe verified revision links may be shown. + +### 6.3 State machine + +| State | Entered when | User-visible meaning | Allowed next states | Recovery/owner | +|---|---|---|---|---| +| Not-applicable | Target unsupported/exempt or no eligible PR link | This command does not apply | None | User selects applicable issue | +| No-published-SDD | Eligible issue has no first publication | No SDD has been published | Compared after gate publication and retry | Issue owner | +| Initial-only | First publication verified, no revision | Only first version exists | Compared after verified revision | Issue owner | +| Current comparison | Digest, branch, publications and fence all verify | Structural comparison is current | Re-run on new source comment | Gate/application | +| Pending-revision | Matching digest; trusted record awaits answers; first publication verified | Revision is being clarified; no current diff | Compared after publication; stale after issue edit | Issue owner | +| Stale | Current digest differs or authority/head fence changes | Current comparison cannot be certified | Retry the read; refresh clarifications if the issue changed | Issue owner | +| Invalid/unavailable | Trust, linkage or provider evidence fails | Current comparison cannot be verified | Retry when evidence available | Maintainer/operator | +| Bounded | Document or work limit exceeded | Exact comparison unavailable within safety bound | None until input changes | Issue/spec owner | + +Duplicate requests follow correlated replay rules. Out-of-order events must pass the final fence or lose current status. Cancellation/provider failure creates no lifecycle mutation; partial provider success is reported only as retained verified facts. + +## 7. User-facing configuration + +| Input | Type | Recommended default | Allowed values/range | Scope/persistence | +|---|---|---|---|---| +| Command | Exact comment text | `/copilot sdd-diff` | No arguments | Per source comment; no durable setting | +| Diff limits | Fixed policy constants | Limits in §4.3 | No user overrides | Repository contract | +| Locale | Existing effective locale | Issue locale; PR locale on PR | English, Spanish; atomic English fallback | Existing locale policy | + +Validation rejects arguments and invalid command forms through existing parser/help. No arbitrary path, ref, URL, limit, or credential is accepted. Existing repository admission/profile precedence applies; the command does not snapshot a new configuration. Limits, read-only behavior, exact identity, and verification requirements are intentionally not configurable. A recommended invocation is `/copilot sdd-diff`; meaningful alternative is the same command on a PR whose native relationship identifies exactly one eligible issue. + +## 8. Clean Architecture + +### 8.1 Responsibilities and dependency direction + +| Layer/boundary | Owns | Must not own/import | +|---|---|---| +| Domain/pure policy | Eligibility result, state precedence, digest decision, line diff bounds, title sanitization | GitHub DTOs, SDKs, I/O | +| Application | Read-only comparison use case, ordered verification and freshness fence | Concrete provider SDK/process concerns or writer ports | +| Adapters/data | Map semantic reads and provider failures; exact PR closing-issue projection | Product state policy | +| Infrastructure/composition | Wire narrowed read ports and existing comment reconciler | Business decisions or extra credentials | +| Entrypoints | Adapt issue/PR event and comment to command input | Duplicate orchestration or inferred identity | +| Presentation | Localized bounded view models and Markdown renderer | Publication mutation or state transitions | + +```mermaid +flowchart LR + E[Comment entrypoint] --> A[Read-only application use case] + A --> D[Pure trust, state and diff policies] + A --> P[Semantic read ports] + I[GitHub and workspace adapters] --> P + A --> V[Localized presentation] + V --> R[Existing correlated reply reconciler] +``` + +### 8.2 Contracts, state, and trust boundaries + +- Pure decisions: target applicability, digest precedence, state mapping, bounded diff/counts, changed-heading extraction, output escaping. +- Application contracts: immutable event identity and operation result; no provider DTOs. Separate comparison reads from reply publication orchestration; the use case receives no write-capable workspace/comment port. +- Semantic ports: reuse/narrow `readSdd`, `verifyPublication`, `isAncestor`, exact `getLinkedBranch`, bound issue comment/title/description/catalog readers. Add a typed read-only PR closing-issue projection through existing GraphQL transport if no exact semantic port exists. Include repository identity and pagination completeness; stop after finding a second eligible issue. +- Durable state/schema ownership: existing bot-owned `readSddGateRecord` schema v1 only. No schema/store changes. +- Concurrency/idempotency: repository, exact target and source-comment correlation through existing reconciler; final issue/record/branch fence immediately before rendering/publishing current claims. +- Trusted inputs: authenticated event identity, canonical repository identity, configured bot ownership, catalog ownership, validated SHA/path and provider-verified linkage/publication. Untrusted: issue/PR/comment text, record content until validated, headings, provider errors and URLs in bodies. +- Provider errors map to bounded unavailable states; raw errors are never echoed. + +### 8.3 Executable architecture constraints + +Architecture tests MUST prove the use case cannot depend on writer ports, model clients, source-edit/branch/approval/lifecycle services, or provider DTOs. Contract tests MUST assert schema-v1 compatibility and exact PR identity/pagination behavior. Dependency/cycle checks and adapter contract fixtures MUST be added to existing repository checks; exact command names are an implementation discovery item. + +## 9. UI/UX and content contract + +### 9.1 Information hierarchy + +Show current status first, verified facts next, then consequence/action, impact and safe evidence links; technical IDs are secondary. Counts and changed titles appear only for a verified current comparison. Historical links in stale/pending states must be explicitly labelled historical. + +### 9.2 Representative primary view + +```markdown + + +# 🔎 SDD structural comparison + +> **Current status:** Compared verified published revisions +> +> **Action required:** No action required + +## Changes + +- First publication: `FIRST_SHA` +- Current publication: `CURRENT_SHA` +- Added lines: 12 · Removed lines: 4 +- Changed sections: “Scope”, “Failure and recovery” + +This is a structural diff. It does not infer semantic requirement changes. + +## Links + +[First SDD](SAFE_IMMUTABLE_DOCUMENT_URL) · [Current SDD](SAFE_IMMUTABLE_DOCUMENT_URL) · [Compare commits](SAFE_COMPARE_URL) +``` + +Concrete variants: + +- **Pending/no action:** “No SDD revision has been published yet” (`no-published-SDD`) or “Only the first SDD publication is available” (`initial-only`); no current comparison claim. +- **Action required:** stale: “The issue description changed after this SDD record. Refresh the SDD gate before comparing.” Link to verified question card; historical publication links only if all required publication/path/ancestry checks pass. +- **Blocked/failed:** “A current comparison is unavailable because the linked branch could not be verified. Retry after the issue’s linked branch is available.” Retain only verified facts; no counts. +- **Partially complete:** “The published documents were verified, but the comparison exceeded the fixed work limit. Exact counts are unavailable.” Provide only safe verified links. +- **Completed unchanged:** “The verified documents have no structural line changes.” Show both SHAs and safe links, zero counts, and the structural-only caveat. + +### 9.3 Issue, PR, and comment behavior + +- Deterministic title: localized equivalent of “SDD structural comparison”; purpose is query result, not a gate/status card. +- Merge/close/retry causes no lifecycle effect. Retrying the same source comment updates its correlated reply; a new source comment creates its own reply. +- One reply per source comment; no extra status cards, notifications, labels or checks. Preserve waiting labels. +- Links are built from policy-owned canonical repository identity plus validated SHA/path. No body-derived URL. Link document, commit and compare evidence descriptively. + +### 9.4 Accessibility, localization, and responsive behavior + +English and Spanish catalogs are complete and selected by existing effective-locale rules; unsupported or invalid catalogs fall back atomically to English. Use textual status, logical headings, short lists and descriptive links; tables are not required in the reply. Must remain readable on narrow screens and light/dark GitHub themes. Escape and bound titles to 120 characters; neutralize mentions, Markdown, control characters and HTML/command markers. Do not echo document contents, raw provider errors or untrusted URLs. Mermaid has the textual flow equivalent in §5. + +## 10. Failure, recovery, and cleanup + +| Failure/partial state | User impact | Retained facts | Automatic retry | Required action | Cleanup | +|---|---|---|---|---|---| +| No publication | No comparison yet | Exact eligible target | On a new command only | Publish SDD through existing gate | None | +| Initial only | No revision pair | Verified first SHA if safe | No | Publish revision | None | +| Stale digest | Current counts withheld | Digest mismatch; safe historical links only after verification | No | Refresh gate/question card | Preserve labels | +| Pending answers | Requested revision is not current | Verified first publication | No | Answer existing clarification | Preserve labels | +| Record/owner/path invalid | Trust cannot be established | Sanitized state only | Bounded provider retry only | Maintainer review | No record repair by command | +| Branch/publication missing | Current comparison withheld | Independently verified facts | Bounded read retry per existing policy | Restore/fix linkage or retry later | None | +| Provider unavailable/incomplete PR pagination | Target/evidence unavailable | Safe event identity only | Existing bounded retry policy | Retry later | None | +| Input/work bound exceeded | Exact diff unavailable | Verified SHAs and safe links | No | Reduce/change SDD within existing constraints | None | +| Final fence changes | Race invalidates current claim | No current counts | Re-run on new event | Retry command | No stale reply claim | + +Errors use `impact -> cause -> action -> retained state`; a failed read must not imply publication or lifecycle failure. + +## 11. Security, permissions, and privacy + +1. Reuse existing least-privilege read permissions and comment-reply authority; comparison workflow itself has no writer port. Confirm exact workflow permission declarations during implementation. +2. Treat comments, issue/PR content, records and headings as untrusted. Bind repository and target to authenticated event; enforce exact admission, bot ownership, catalog path, branch and publication proofs. +3. Read only allowlisted spec content at immutable verified SHAs. Never accept arbitrary refs/paths/URLs or expand local setup credentials. Do not log or reply with file contents, tokens, PII beyond existing issue identity, or raw provider errors. +4. Resist replay/forgery through validated record ownership, exact source-comment correlation, bounded processing, output escaping, and final freshness fencing. + +## 12. Observability and operational UX + +- User-facing states are the state table in §6.3; distinguish not-applicable, stale, pending, invalid, missing and unavailable. +- Job Summary is not needed for a comment query; do not add one absent an existing operational convention. +- Logs may record correlation ID, state category, validated SHAs and bound outcome, but not document content or raw errors. Metrics should use bounded state labels; exact existing telemetry is unknown. +- Provider unavailability is not a failed SDD publication. Respect existing rate-limit/retry policy; no unbounded retries. +- Noise budget is one correlated reply per source comment, no notifications/status cards. + +## 13. Compatibility, migration, rollout, and rollback + +- Existing gate records and in-flight operations remain unchanged. Schema v1 is read as-is; malformed/foreign/unknown version fails closed. +- No config or record migration, deprecation, or feature flag is proposed. Existing command help/parser and orchestration are extended compatibly. +- Rollout adds command/docs/catalog evidence after acceptance checks. Rollback removes command dispatch while retaining existing gate records and behavior; no irreversible SDD or lifecycle effects are introduced. + +## 14. Testing strategy and numeric budget + +Minimum: **250 distinct extension cases**, with a 24-case risk floor exceeded by the concrete inventory below. Existing shared gate/status/publication cases remain unchanged and are not counted again. + +| Area | Distinct cases | Behaviors/risks covered | +|---|---:|---| +| Pure bounded diff | 18 | Exact line/section changes, fenced headings, hostile input, all fixed resource ceilings | +| Presentation/localization/navigation | 67 | Ten states in both catalogs, atomic fallback, immutable commit/blob/compare links, clarification cards, escaping and bounds | +| Application/authority/fences | 77 | Admission, missing/invalid/pending/stale authority, immutable reads, issue/card/head/PR-link races, exact author and no writes | +| Native PR-link adapter | 45 | Bound repository ID/name and head repository, exact PR/head/ref, author identity, complete pagination, malformed/foreign/duplicate rows and denial | +| Read-only composition | 5 | Frozen repository binding, trusted server origin, native event repository ID and receiver-bound document read without writers | +| Parser/routes/agent activity | 15 | Whitespace/case, no arguments, EN/ES/fallback, absent query, no agent activation or activity | +| Correlated publication/replay | 18 | Same-source refresh, unchanged replay, distinct source, ownership/correlation/key isolation, duplicate healing, static catalogs and final sanitization | +| Actual Execution projection | 5 | Native repository ID/issue author/profile digest, immutable input, missing/zero/unsafe identity and native PR target | +| **Total** | **250** | Every case counted once | + +The two new shared `SddEvidenceQueryUseCase` opaque-authority regressions are allocated only to the owning status companion (182 + 2 = 184); they are excluded from this 250-case budget. The gate's separate 149-case budget and both communication ledgers remain unchanged. + +Repository-wide thresholds remain 90% lines/statements, 88% functions and 82% branches. `scripts/coverage-budgets.json` additionally enforces 100% in every new pure module, and at least 95% lines/statements and 90% branches/functions in each new application, native adapter and composition module. All six new runtime modules currently achieve 100% in the focused scope. Deterministic fakes and actual Execution constructors provide local proof; live GitHub runs independently establish bot permissions, event wiring and human UX. + +Required distinct risk coverage includes eligibility/identity (4), record/ownership/publication trust (5), digest/revision races (4), diff/count/bounded work (5), and localization/sanitization/replay/no-write (6), totaling at least 24 cases within the 30-case distribution. + +## 15. Documentation and discoverability + +| Audience | Artifact/page | Required content | Validation/navigation | +|---|---|---|---| +| User | `docs/issues/comment-commands.mdx` | Exact command, eligible targets, examples, states and links | Command catalog and link checks | +| Issue owner | `docs/issues/pre-branch-sdds.mdx` | First/current revision distinction, stale/pending recovery, structural-only caveat | Examples checked against fixtures | +| Operator | Same SDD guide | Invalid/unavailable evidence, retry and permission boundaries | State decision table review | +| Contributor | Capability SDD and catalog evidence | Ports, trust boundaries, 250-case budget, workflow checks | `pnpm run validate:specifications`; catalog generation/check as repository requires | + +Docs must explain PR exact-link rules, limits, no-write behavior and that line/title differences do not establish semantic requirement changes. Update the capability catalogue evidence and owning SDD or this approved bounded companion before branch creation. Keep documentation examples synchronized with fixtures. + +## 16. Acceptance + +1. Given an admitted eligible issue with a valid schema-v1 record, matching digest, exact linked branch, verified first/current publications and successful final fence, when a participant comments `/copilot sdd-diff`, then one localized reply shows both SHAs, safe immutable links, exact added/removed line counts and at most eight changed titles, and states that the result is structural only. +2. Given an eligible issue with no publication, when queried, then it reports `no-published-SDD` without calling it invalid or ready. +3. Given a verified first publication and no revision, when queried, then it reports `initial-only` and does not invent a comparison. +4. Given a matching digest and awaiting-answer record with verified first publication, when queried, then it reports pending-revision with no current counts. +5. Given any current digest mismatch, including an awaiting-answer record, when queried, then stale takes precedence over pending; no current counts appear and the question-card link is provided. +6. Given a foreign, forged, malformed or unknown-version record, when queried, then no current comparison is claimed and no untrusted values are rendered as authority. +7. Given wrong owner/path, absent publication, deleted or mismatched branch, failed ancestry/publication proof, when queried, then the result is invalid, missing or unavailable as supported by verified facts, never current-ready. +8. Given a PR with exact native same-repository closing-issue authority to zero eligible issues, when queried, then it reports not-applicable; with two SDD-eligible issues or disagreeing identity it reports ambiguous/unavailable. +9. Given incomplete PR pagination or unreadable authority, when queried, then it reports unavailable and does not fall back to body numbers or equal PR/issue numbers. +10. Given a document over any character, byte or line limit, or computation over 2,000,000 steps, when compared, then it reports bounded/unavailable without truncation or guessed counts. +11. Given more than eight changed section titles or unsafe headings, when rendered, then only up to eight escaped, bounded titles appear and the omitted count is explicit; mentions/control/Markdown injection is inert. +12. Given a document with no line changes, when queried, then it reports zero structural changes with verified links. +13. Given replay of the same source comment, when processed again, then the existing correlated reply is updated; a distinct source comment receives its own reply. +14. Given issue, record or branch mutation between collection and publication, when the final fence runs, then current counts and ready claims are withheld. +15. Given provider errors, when rendered, then raw error text and document contents are absent and impact/cause/action/retained-state order is used. +16. Given unsupported locale or invalid locale catalog, when rendered, then a complete English response is used atomically; supported issue/PR locales use English/Spanish catalogs respectively. +17. Given any invocation, when inspecting effects, then no model, source edit, branch, approval, label, lifecycle mutation, extra status card or notification occurred. +18. Given the implementation, when architecture/workflow checks run, then application dependencies contain no writer/model capability and exact PR identity and read-only permissions are structurally verified. +19. Given docs and catalog updates, when repository specification validation runs, then links/evidence are current and examples agree with executable fixtures. +20. Given issue #454's real first and revised SDD, when reviewers inspect the generated UX fixture, then the structural-only distinction and immutable navigation are understandable without inferring semantic requirements. + +## 17. Requirements traceability + +| Requirement | Policy/use case/adapter/presentation | Test or evidence | Documentation | +|---|---|---|---| +| Exact target and admission | Entry adapter, admission policy, PR-link port | Identity/admission and pagination contracts | Command guide | +| Trusted record and current readiness | Gate reader, digest/publication/branch policies | Forgery, owner, ancestry, race cases | SDD guide | +| Bounded structural result | Pure diff policy, read-only use case | Character/byte/line/step and count tests | Both user pages | +| Safe correlated reply | Renderer/reconciler | Locale, sanitization, replay tests | Command guide | +| Read-only architecture | Application ports/composition | Dependency and workflow permission checks | Contributor SDD | + +## 18. Implementation sequence + +1. Confirm existing parser, gate record schema, catalog ownership/evidence and repository coverage/workflow contracts; add exact PR closing-issue semantic projection if absent. +2. Define immutable read-only contracts and enforce absence of writer/model dependencies. +3. Implement pure eligibility, state precedence, bounded diff and sanitization policies with tests. +4. Implement ordered application reads, exact linkage/publication checks and final freshness fence. +5. Add adapter mappings and structural PR identity/pagination tests. +6. Add localized states and correlated reply integration with golden and semantic fixtures. +7. Update both issue docs, capability catalogue evidence and this companion/owning SDD before branch creation. +8. Complete 250-case minimum, all existing gate budgets, #454 UX review, architecture/workflow checks and specification validation. + +## 19. Definition of Done + +- [ ] Every normative requirement maps to acceptance and traceability. +- [ ] Existing SDD gate contract, record schema and budgets remain unchanged. +- [ ] Exact issue/PR identity, owner, path, branch, digest, publications and final fence are verified. +- [ ] Read-only architecture is enforced; no model, source, branch, approval or lifecycle capability is reachable. +- [ ] All fixed input/work/output limits and state distinctions pass. +- [ ] At least 30 distinct risk-derived cases pass, including all 24 named minimum risk categories; coverage meets repository thresholds and new pure policy branch target. +- [ ] English/Spanish, fallback, sanitization, replay, link safety and comment budget pass. +- [ ] Docs and catalog evidence are current; #454 human UX evidence is reviewed. +- [ ] Failure, race, unavailable, bounded and recovery behavior passes without false current claims. +- [ ] `pnpm run validate:specifications` passes and catalog output is current according to repository workflow. +- [ ] No readiness-blocking decision remains unresolved. + +## 20. References and decisions + +- Primary sources: Issue #464 and its explicit answer rounds supplied with this request; existing repository contracts named in those answers, pending source-path confirmation. +- Related specs/ADRs/issues/PRs: owning `issue-start-and-sdd-readiness` SDD; live issue #454 as UX evidence; exact URLs and catalog identifiers beyond the supplied capability ID remain to be confirmed from repository metadata. +- Decisions: companion SDD under the existing capability; exact native same-repository PR closing-issue relationship is authoritative; digest mismatch always reports stale; pending-revision requires matching digest and verified first publication; first publication fields remain immutable; no arbitrary paths, refs, body URLs, semantic inference, model, writes or new record store. +- Follow-up outside this spec: any broader semantic SDD analysis, changes to gate lifecycle/record schema, or general PR-to-issue discovery. + +### Confirmed implementation bindings and dependency gate + +The verified shared query is `SddEvidenceQueryPort` in +`src/application/ports/sdd_evidence_read_ports.ts`, implemented by +`sdd_evidence_query_use_case.ts` from PR #465. Its closed snapshot distinguishes +not-required, absent, unavailable, invalid, stale, awaiting-answers, +publication-pending and published. Verified evidence contains the owning path, +capability, exact branch name/head and immutable first/current SHAs. The diff +must consume this query rather than duplicate its gate/digest/ancestry logic. +`PreBranchSddWorkspacePort.readSdd(commitSha, path)` supplies the two immutable +contents through a physically read-only projection; no draft, publish or repair +method may reach the command. + +Composition must provide only the shared query, immutable content read and exact +native PR closing-issue query. Both native association and shared evidence are +reread after collecting the documents, with all relevant identities compared +before any current count is rendered. A pending revision may expose a verified +first publication as history, never current counts. An original-only publication +has one immutable version and no fabricated second revision. Unavailable or +bounded comparison produces no guessed counts or silently truncated documents. + +### Partial implementation evidence (2026-10-12) + +The initial local 33-case observation was 11 new comparator cases plus 22 +pre-existing shared card/digest cases. The latter are preserved separately and +excluded from this extension; no shared gate or digest implementation is copied +into the diff feature. + +The current independent contribution has **106 distinct new cases**: + +| Implemented area | Distinct new cases | Verified risks | +|---|---:|---| +| Pure structural comparator | 18 | Exact line counts, honest character/byte/line/work ceilings, matching Markdown fences, bounded title changes and propagation of unexpected computation faults | +| Localized presentation | 52 | Ten states in English/Spanish, atomic fallback, historical-only pending/stale proof, exact safe repository links, missing projections, hostile headings, omitted titles and the 5,600-character body ceiling | +| Native PR closing-issue adapter | 36 | Exact repository IDs/names, PR number/head, complete bounded inventory, duplicate/foreign/malformed rows, denials, immutable binding/output and a constant read-only query | +| **Implemented partial total** | **106** | No shared helper case counted again | + +All four new runtime modules reach 100% lines/statements/branches/functions in +the focused scope. Typecheck and lint passed. Full validation in the existing +older diff worktree passed 563 suites, 7,270 tests and 26 configured skips +(7,296 total), including every unchanged official coverage budget. The setup +acceptance ledger retains 308/350 passing cases and 42 explicitly open cases. +This is partial validation on that worktree's ancestor, not proof of a combined +implementation on current master or the shared foundation. + +A read-only operator GraphQL query on real PR #465 confirmed the exact native +contract: repository database ID/name, exact PR number/head, sole same-repository +closing issue #454, totalCount 1 and hasNextPage false. It establishes response +shape only. Bot permission, application orchestration, final evidence/link +fences, parser/help/composition, correlated publication and live issue/PR UX +remain unverified. The final numeric distribution must include their distinct +risk cases before publication. The specification remains Draft. + + +### Integrated local implementation (2026-10-12) + +The earlier 106-case report is a historical partial snapshot. The current implementation has 212 dedicated cases across seven suites plus five actual Execution producer contracts, totaling 217 without shared-case duplication. The six new runtime modules have 100% lines/statements/branches/functions in focused coverage. Native Action-only synchronization committed `ad27952768674c11bb225bcf9389917c3c4b7f9f` and later `0f016278e34a7cbba0bfa4ad290e759492064994`; no manual parent merge or foundation copy was used. + +The shared snapshot now carries an optional opaque SHA-256 authority digest over the exact fenced card and live issue authority. It exposes no card contents or raw provider data. The diff application requires that digest before current claims and compares it with canonical publication facts after reading immutable documents. Native PR relationships additionally bind current head repository, ref, SHA and original issue author, including a final association fence. + +Only the `copilot-sdd-diff` reply family refreshes its existing exact bot-owned target/source correlation when the newly verified reading changes. Unchanged replay makes no mutation; other reply families retain their existing create-once behavior. Complete static catalogs also bypass dynamic localization for this command. Publication still follows the ordinary non-atomic GitHub comment boundary: the final authority fence prevents changes observed during collection, but does not claim a server-side compare-and-swap transaction. + +Live command activation, #454 first/revised UX, PR native-link acceptance, replay and locale checks remain pending promotion. Local coverage is not proof of those outcomes. + + +Stale presentation does not invent a cause: a card or head race is not described as an issue edit. The next action retries the current read first and directs the owner to clarifications conditionally when the issue itself changed. Existing state/fence cases cover this wording without adding mirrored prose tests or inflating the 240-case budget. + + +Final local integration on Action merge `86db9b1962915111f02613b4f49ec50a6a3f1cf6` passes the official full validation: 579 suites, 7,952 passing tests and 35 unchanged platform skips. Overall coverage is 97.25% lines, 96.23% statements, 92.70% branches and 97.10% functions; every new comparison module remains at 100%. Type, lint, workflow, documentation/specification validation, generated catalog and graph update pass. Live whitespace/case/newline synchronization run `38253646042` also passed on Windows with all five configured checks before publishing that parent merge. This proves operational synchronization and combined local source; it does not yet activate the new command on master. + + +### Independent review: native eligibility and inert results (2026-10-12) + +PR #469 review finding `4249066303` correctly identified that the provisional all-links ambiguity rule was stricter than the requested exactly-one-eligible behavior. The query now reads bounded native candidate admission and the existing issue-start/SDD requirement policy, selects one eligible candidate, and rechecks that selection before returning counts. It rejects unreadable candidates and the fixed 100-candidate ceiling, and retains the native head/ref/repository/author fence. Eleven distinct cases cover eligible + help/exempt links, selected author, zero/multiple eligible candidates, unreadability, lost/new/swapped eligibility, missing author and the ceiling. + +Finding `4249066310` was a false positive: the shared selector already returns before this branch when `result.executed` is false. One regression using Result's default false value proves no comment read or write; the maintainer recorded that evidence and invoked `/copilot dismiss` instead of adding a redundant guard. The current feature inventory is 217 + 11 + 1 = 240. Shared status/authority cases remain outside that total. + + +### Live first publication and independent final-association regressions (2026-10-13) + +Native PR-review command delivery on PR #469 now resolves issue #464 and updates +the existing reply in place to verified first publication at +`82bc9be5eae5170a669313a467cb71919eaff5a1`. The prior not-applicable result was +caused by missing effective branch/SDD flags in repository callbacks; parent +`aa10ee66` supplies the installed configuration consistently. No second +publication or complete live comparison acceptance is claimed by that result. + +Finding `4253073961` remained valid despite automatic classification as obsolete. +Eleven new regressions failed before the correction; all 74 query cases now pass. +Every derived PR result, including absent, pending, stale and exempt states and a +changed final authority, rereads the exact native association before publication. +A moved association returns stale without retaining old publication/card links or +counts; unavailable or foreign final PR identity returns unavailable. Stable +pending history remains visible. This adds eleven cases exactly once (229 + 11 = +240); the shared companion remains 184. Full gates and live replay remain pending. + +### Final candidate eligibility and historical revision links (2026-10-13) + +Native findings `4254268292` and `4254268297` exposed two gaps. A PR with no selected eligible issue MUST re-admit its candidates before returning exemption or ambiguity; changed outcomes become stale and denied reads become unavailable. Stable native PR associations alone cannot certify candidate eligibility. Stale or pending replies MUST retain each verified distinct publication as explicitly historical, including the published revision; they MUST withhold current comparison/counts and reject malformed revision proof atomically. + +Ten additional distinct cases cover three candidate outcomes (exempt becomes eligible, ambiguous becomes selected, denied final re-admission) and seven historical presentation outcomes (both states in both locales, no revision, duplicate immutable SHA, malformed revision). The dedicated inventory becomes 240 + 10 = **250**; shared companion cases remain allocated only to their owner. English/Spanish labels distinguish historical first publication and historical published revision. Documentation and all existing bounds/coverage thresholds remain in force. + + +### Request witness after shared publication integration (2026-10-13) + +The Action-managed Core integration at `94653aa5` retained a feature-only +refresh exception for `copilot-sdd-diff` when no source reader was supplied. +Publication MUST fail closed before inventory or any create/update/delete/ +compaction when its request witness or reader is absent or the source is denied. +The special fallback is removed; normal SDD comparison replies use the shared +correlated source witness and refresh path. The witness remains private and +adds no model, permission or public input. Positive publication fixtures now +provide the same physically bound evidence expected in production. + +Four distinct cases cover missing witness/reader creation, missing-evidence +refresh and duplicate cleanup, reader absence with a witness, and source denial. +Two reproduced the bypass before correction; two are retained negative controls. +The comparison inventory is **254** (250 + 4); the independently allocated +shared authority inventory is unchanged. Global/module coverage and native +reply replay and revision-change acceptance remain required. diff --git a/specs/pre-branch-sdd-status-readiness.md b/specs/pre-branch-sdd-status-readiness.md index a8f0897d1..b69324744 100644 --- a/specs/pre-branch-sdd-status-readiness.md +++ b/specs/pre-branch-sdd-status-readiness.md @@ -1,9 +1,9 @@ # Pre-branch SDD Readiness in `/copilot status` -- Status: Proposed +- Status: Implemented - Date: 2026-10-10 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: 2026-10-11 +- Last verified: 2026-10-12 - Owners: Copilot maintainers - Scope: Add a localized, read-only view of pre-branch SDD readiness to the existing issue status answer. - Related issues/PRs: Issue #454; existing owning specification `specs/pre-branch-sdd-gate.md` @@ -524,11 +524,17 @@ graph update and build passed. A fresh native status request remains required; these offline checks do not certify that live result. +### Opaque authority identity for structural comparison (2026-10-12) + +The shared read-only snapshot optionally exposes `authorityDigest`, a SHA-256 fingerprint of the exact fenced bot-owned card body, canonical current issue digest/labels and relevant awaiting-answer provenance. It exposes none of those contents. Existing status presentation ignores this optional field and the stored schema-v1 gate record is unchanged. The structural comparison consumer requires and rechecks this digest, so a card-body mutation that preserves the visible publication fields cannot reuse old current counts. + +Two distinct authority-reader regressions prove stable opaque output and changed identity after a same-projection card-body replacement. These two cases are allocated once to this owning authority-reader inventory: the corrected default-branch 182 plus two is 184. They are excluded from the separate 229-case structural comparison inventory. Prior dated validation counts remain historical evidence. + ### Shared workflow coverage for independent SDD reads (2026-10-12) Readiness parent integration `38255933907` passed its full Jest cases but failed the stricter per-module coverage gate because the existing SDD status bridge lacked direct workflow tests. Six contracts now exercise verified/unavailable projection independently of the PR adapter and exclude foreign issue, missing initial readiness, PR target and missing context. They cover the existing bridge without copying shared implementation into a managed feature or lowering a coverage threshold. -These six shared status cases raise its default-branch authority allocation from 65 to 71 and total from 198 to 204. The separate structural comparison contribution adds two opaque-authority cases; when both contributions are integrated the companion total is 206, never 204 + the full comparison inventory. The gate's 149 cases remain separate. +These six shared status cases were added to the authority/application/fences allocation. The separate structural comparison contribution retains its own tests and coverage entry; the gate's 149 cases remain separate. ### Correct the distinct-case inventory (2026-10-12) @@ -546,13 +552,26 @@ functional assertion and risk requirement remains; coverage thresholds, the Historical physical Jest totals remain execution evidence; earlier extension figures 141/181/198/204 included these 22 copies and must not be read as independent behaviors. Their corrected distinct counts are 119/159/176/182. -The separate comparison's two opaque-authority contracts would make 184 when -integrated; its entire test inventory is never added to this companion. +The default-branch correction is 182. On this combined head, the comparison's +two authority-reader contracts are allocated here once, making 184; the +separate structural comparison inventory remains 229 and excludes them. Focused status tests and full coverage MUST retain the same production-module coverage after deduplication. No runtime, label, model, platform, publication or deployment behavior changes in this correction. +### Reconcile the allocation after native parent integration (2026-10-13) + +Action merge `139955e8` preserved both opaque-authority regression bodies but +rewrote their allocation outside this companion while the comparison SDD still +excluded them. The corrected non-overlapping inventory is 73 authority cases +and 184 total here; the comparison's 229 and gate's 149 remain separate. This +repairs contradictory metadata only: no test is added, removed or counted twice, +and the 22 deleted copies are not restored. Existing tests and thresholds stay +unchanged. Prior physical Jest totals remain dated execution evidence. The +subsequent closed-history corrections below supersede this intermediate +184-case allocation with the current 194-case total. + ### Native closed history (2026-10-13; local correction, live acceptance pending) Removing `in-progress` after verified native completion MUST NOT erase an diff --git a/specs/semantic-github-publication-and-notification.md b/specs/semantic-github-publication-and-notification.md index 9654e487b..b309c5638 100644 --- a/specs/semantic-github-publication-and-notification.md +++ b/specs/semantic-github-publication-and-notification.md @@ -1441,6 +1441,12 @@ The repair owns **30 distinct cases**: 26 provider-bound immutable-comparison ca Live human doctor push `38162035628`, head `25950008`, exposed a second comparison-boundary problem: 39 files contained 248,563 raw source-patch bytes, including 89,186 bytes of the generated specification index; JSON escaping exceeded the unchanged 256,000-byte budget. The exact generated `specs/CATALOG.md` path now carries metadata only, alongside build outputs. The editable `specs/catalog.json` still requires its full patch. Two distinct provider contracts cover the generated index and its source. Bounds/incomplete data now report `provider.unavailable`; only invalid or contradictory object identity uses `workflow.stale`, so the Job Summary does not falsely claim the branch advanced. Fresh live acceptance remains pending. +### Read-only SDD comparison reply refresh (2026-10-12) + +`/copilot sdd-diff` adds the closed `copilot-sdd-diff` correlated reply family. Its already bounded EN/ES presentation is selected only from an executed successful `.SddDiff` result with a valid resolved locale. Composition uses the static publication catalog for this deterministic reply, without invoking translation or an agent. + +For this family only, a repeated source comment refreshes the exact bot-owned target/correlation/message-key canonical reply when the latest verified reading changes; an identical replay is unchanged. Other reply families retain create-once behavior. Existing owned duplicate cleanup remains applicable and foreign markers are inert. The final sanitizer neutralizes automation markers and mentions. These 17 dedicated cases are allocated to the structural comparison's 217-case inventory, not recounted in the existing 135-case minimum or communication ledgers. Authority/freshness belongs to the comparison application and its shared evidence query; comment publication does not claim an atomic GitHub transaction. + ### Current request reply recovery: dogfooding #474 (2026-10-13) Native run `38331274938` failed after actual Codex execution, but its source @@ -1702,3 +1708,22 @@ the unchanged 28,000-unit ceiling. Two additional cases cover withholding the unsafe boundary and all safe near-limit replays; one failed before correction. The current inventory is 89 runtime cases (39 pure, 33 retirement, 2 coordination, 11 publication and 4 analysis) and 45 conservative declarations. + + +### Missing-reader integration regression (2026-10-13) + +The Action-owned SDD comparison integration `20e817bd` retained the comparison +source guard but restored a legacy fallback for other comment-correlated +replies when both witness and reader were absent. Three distinct regressions +failed before correction: ordinary issue-comment reply, review-comment reply, +and immutable mutation receipt. Every `comment:` correlation now requires its +private native request witness before any comment read or publication effect, +independently of whether a reader was bound. Verified read-only refreshes and +non-comment immutable publication retain their existing contracts. + +These **three cases** belong only to semantic publication, bringing that +request-witness extension from 56 to **59** cases; the original cross-capability +allocation becomes **76** (59 + 11 execution + 6 autofix). They are excluded +from the SDD comparison's separate 254-case inventory. No permission, +configuration, coverage threshold or platform changes. Documentation of the +source-freshness boundary remains in the comment-command guide. diff --git a/src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts b/src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts new file mode 100644 index 000000000..3b2d87e5c --- /dev/null +++ b/src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts @@ -0,0 +1,143 @@ +import { renderSddDiff, type SddDiffSnapshot } from '../sdd_diff_presentation_policy'; +import { resolveSddDiffCatalog, SDD_DIFF_MESSAGE_IDS, ENGLISH_SDD_DIFF_DEFINITION, SPANISH_SDD_DIFF_DEFINITION } from '../sdd_diff_message_catalog'; +const first = 'a'.repeat(40), current = 'b'.repeat(40); +const publication = { path: 'specs/owning-sdd.md', firstSha: first, currentSha: current }; +const scope = { serverOrigin: 'https://github.example.test', owner: 'org', repository: 'repo' }; +const compared: SddDiffSnapshot = { state: 'compared', publication, + changes: { state: 'compared', addedLines: 0, removedLines: 0, sections: [], omittedSections: 0 } }; +const snapshots: readonly SddDiffSnapshot[] = [ + ...(['not-applicable', 'no-publication', 'invalid', 'unavailable', 'ambiguous'] as const).map(state => ({ state })), + { state: 'initial-only', publication }, { state: 'stale', publication }, { state: 'pending-revision', publication }, + { state: 'bounded', publication, reason: 'lines' }, compared, +]; +const render = (value: SddDiffSnapshot, locale = 'en-US') => renderSddDiff(value, scope, resolveSddDiffCatalog(locale)); +describe('bounded bilingual structural SDD reply', () => { + it.each(['en-US','es-ES'].flatMap(locale => (['stale','pending-revision'] as const).map(state => ({ locale, state }))))('retains the verified published revision as historical in $state / $locale', ({ locale, state }) => { + const body = render({ state, publication }, locale); const catalog = resolveSddDiffCatalog(locale); + expect(body).toContain(locale === 'es-ES' ? 'Revisión publicada histórica' : 'Historical published revision'); + expect(body).toContain(`/blob/${current}/specs/owning-sdd.md`); expect(body).toContain(`/commit/${current}`); + expect(body).not.toContain('/compare/'); expect(body).not.toContain(catalog.message('sddDiff.current')); + }); + it('retains only first history when no verified revision exists', () => { + const body = render({ state: 'stale', publication: { path: publication.path, firstSha: first } }); + expect(body).toContain(`/blob/${first}/`); expect(body).not.toContain('Historical published revision'); + }); + it('does not duplicate the same historical publication', () => { + const body = render({ state: 'stale', publication: { ...publication, currentSha: first } }); + expect(body.split(`/blob/${first}/`).length - 1).toBe(1); expect(body).not.toContain('Historical published revision'); + }); + it('rejects malformed historical revision proof without partial links', () => { + const body = render({ state: 'pending-revision', publication: { ...publication, currentSha: 'abc' } }); + expect(body).toContain('could not be verified'); expect(body).not.toContain('/blob/'); + }); + + it.each(snapshots.flatMap(snapshot => ['en-US', 'es-ES'].map(locale => ({ snapshot, locale }))))('renders $snapshot.state in $locale without inventing counts or comparison links', ({ snapshot, locale }) => { + const catalog = resolveSddDiffCatalog(locale), body = render(snapshot, locale); + expect(body).toContain(catalog.message(`sddDiff.${snapshot.state}`)); + expect(body).toContain(catalog.message('sddDiff.action')); + expect(body.length).toBeLessThanOrEqual(5600); + if (snapshot.state === 'compared') expect(body).toContain(catalog.message('sddDiff.lines', { added: 0, removed: 0 })); + else expect(body).not.toContain(catalog.message('sddDiff.lines', { added: 0, removed: 0 })); + if (snapshot.state === 'stale' || snapshot.state === 'pending-revision') { + expect(body).toContain(catalog.message('sddDiff.historical')); + expect(body).toContain(`/blob/${first}/specs/owning-sdd.md`); + expect(body).toContain(current); + expect(body).toContain(catalog.message('sddDiff.historicalCurrent')); + } + if (snapshot.state === 'compared' || snapshot.state === 'bounded') expect(body).toContain(`/blob/${current}/specs/owning-sdd.md`); + if (snapshot.state === 'bounded') expect(body).toContain(catalog.message('sddDiff.linesLimit')); + if (snapshot.state === 'initial-only') expect(body).not.toContain(current); + }); + it('constructs links at the bound enterprise origin rather than github.com or supplied heading URLs', () => { + expect(render(compared)).toContain(`https://github.example.test/org/repo/blob/${current}/specs/owning-sdd.md`); + expect(render(compared)).not.toContain('github.com'); + }); + it('falls back atomically to English for an unsupported locale without a model', () => { + expect(render(compared, 'fr-FR')).toBe(render(compared, 'en-US')); + }); + it('provides complete matching English and Spanish message inventories', () => { + for (const catalog of [ENGLISH_SDD_DIFF_DEFINITION, SPANISH_SDD_DIFF_DEFINITION]) { + expect(Object.keys(catalog.messages).sort()).toEqual([...SDD_DIFF_MESSAGE_IDS].sort()); + expect(SDD_DIFF_MESSAGE_IDS.every(id => typeof catalog.messages[id] === 'string')).toBe(true); + } + }); + it('escapes section Markdown, mentions and controls without exposing document excerpts', () => { + const body = render({ ...compared, changes: { state: 'compared', addedLines: 1, removedLines: 1, + sections: ['[click](javascript:alert(1)) @all