From 7ae2878bae22e6c38f24dd00e35face71c15344d Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Fri, 30 Oct 2026 22:27:29 +0100 Subject: [PATCH] bugfix-808-pre-branch-sdd-missing-trusted-catalog-is-hidden-behind-generic-workflow-failed-and-retry-guidance: diagnose missing trusted catalog --- build/api/index.js | 2 + .../api/src/data/model/application_error.d.ts | 3 +- build/cli/index.js | 66 +++++++++- build/deployment_recovery/index.js | 12 ++ build/github_action/index.js | 66 +++++++++- docs/issues/pre-branch-sdds.mdx | 26 ++++ .../operations/error-reference.mdx | 8 ++ specs/CATALOG.md | 6 +- specs/catalog.json | 8 +- specs/pre-branch-sdd-gate.md | 113 +++++++++++++++++- ...lication_error_presentation_policy.test.ts | 5 + .../sdd_preparation_status_policy.test.ts | 32 +++++ .../application_error_message_catalog.ts | 10 ++ .../sdd_preparation_message_catalog.ts | 2 + .../policies/sdd_preparation_status_policy.ts | 11 +- .../sdd/pre_branch_sdd_gate_use_case.ts | 11 +- .../pre_branch_sdd_gate_use_case.test.ts | 76 ++++++++++++ src/data/model/application_error.ts | 3 + .../sdd_trusted_catalog_failure.test.ts | 24 ++++ src/domain/sdd_trusted_catalog_failure.ts | 14 +++ .../pre_branch_sdd_workspace_adapter.test.ts | 54 +++++++++ .../pre_branch_sdd_workspace_adapter.ts | 4 + 22 files changed, 533 insertions(+), 23 deletions(-) create mode 100644 src/domain/__tests__/sdd_trusted_catalog_failure.test.ts create mode 100644 src/domain/sdd_trusted_catalog_failure.ts diff --git a/build/api/index.js b/build/api/index.js index 7131071c7..14a6e7a6b 100644 --- a/build/api/index.js +++ b/build/api/index.js @@ -6200,6 +6200,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-catalog-unsupported-profile', 'sdd-catalog-format-invalid', + 'sdd-catalog-missing-trusted-source', 'sdd-draft-format-invalid', 'sdd-draft-case-identity-invalid', 'sdd-draft-case-budget-invalid', @@ -6446,6 +6447,7 @@ const RECOVERY_VARIABLE_KEYS = Object.freeze({ 'sdd-draft-size-exceeded': Object.freeze([]), 'sdd-catalog-unsupported-profile': Object.freeze([]), 'sdd-catalog-format-invalid': Object.freeze([]), + 'sdd-catalog-missing-trusted-source': Object.freeze([]), 'sdd-draft-format-invalid': Object.freeze([]), 'sdd-draft-case-identity-invalid': Object.freeze([]), 'sdd-draft-case-budget-invalid': Object.freeze([]), diff --git a/build/api/src/data/model/application_error.d.ts b/build/api/src/data/model/application_error.d.ts index 0401515a8..705b97d5d 100644 --- a/build/api/src/data/model/application_error.d.ts +++ b/build/api/src/data/model/application_error.d.ts @@ -11,7 +11,7 @@ import { type BranchSyncVerificationFailure } from '../../domain/verification_fa import { type BranchSyncCatalogDiagnostic } from '../../domain/branch_sync_catalog_diagnostic'; export type ApplicationErrorKind = 'configuration' | 'authorization' | 'provider' | 'agent' | 'validation' | 'workflow' | 'unknown'; export type ApplicationErrorCode = 'configuration.invalid' | 'configuration.unsupported' | 'authorization.denied' | 'authorization.credential-invalid' | 'provider.not-found' | 'provider.conflict' | 'provider.rate-limited' | 'provider.unavailable' | 'provider.contract-invalid' | 'agent.policy-rejected' | 'agent.failed' | 'locale.output-invalid' | 'locale.translation-failed' | 'validation.invalid-input' | 'validation.evidence-too-large' | 'validation.evidence-unavailable' | 'workflow.invalid-event' | 'workflow.stale' | 'workflow.cancelled' | 'workflow.failed' | 'workflow.presentation-pending' | 'timeout' | 'unexpected'; -export declare const APPLICATION_ERROR_RECOVERY_IDS: readonly ["comment-command-length", "comment-command-name-missing", "comment-command-name-unknown", "comment-command-argument-count", "comment-command-arguments-unexpected", "comment-command-arguments-missing", "comment-command-finding-quoting", "comment-command-review-format", "comment-command-review-effort", "comment-command-review-boolean", "comment-command-review-option", "comment-command-sync-option", "comment-command-sync-parent", "pull-request-link-restored", "pull-request-link-base-retained", "pull-request-link-reference-retained", "pull-request-link-base-and-reference-retained", "managed-branch-enrichment-failed", "inactivity-explanation-failed", "bugbot-review-blocks-pending", "branch-sync-agent-disabled", "branch-sync-fixer-unavailable", "branch-sync-resolution-invalid", "branch-sync-verification-failed", "branch-sync-catalog-incompatible", "sdd-draft-size-exceeded", "sdd-catalog-unsupported-profile", "sdd-catalog-format-invalid", "sdd-draft-format-invalid", "sdd-draft-case-identity-invalid", "sdd-draft-case-budget-invalid", "sdd-draft-requirement-mapping-invalid", "sdd-draft-repository-path-invalid", "deployment-publication-context-missing", "workspace-changes-rejected", "closed-issue-reconciliation-failed", "approval-evidence-read-failed", "change-size-reconciliation-failed", "issue-workflow-admission-blocked", "progress-binary-evidence-unavailable", "progress-result-contract-invalid", "stored-configuration-schema-missing", "stored-configuration-schema-unsupported", "stored-configuration-invalid"]; +export declare const APPLICATION_ERROR_RECOVERY_IDS: readonly ["comment-command-length", "comment-command-name-missing", "comment-command-name-unknown", "comment-command-argument-count", "comment-command-arguments-unexpected", "comment-command-arguments-missing", "comment-command-finding-quoting", "comment-command-review-format", "comment-command-review-effort", "comment-command-review-boolean", "comment-command-review-option", "comment-command-sync-option", "comment-command-sync-parent", "pull-request-link-restored", "pull-request-link-base-retained", "pull-request-link-reference-retained", "pull-request-link-base-and-reference-retained", "managed-branch-enrichment-failed", "inactivity-explanation-failed", "bugbot-review-blocks-pending", "branch-sync-agent-disabled", "branch-sync-fixer-unavailable", "branch-sync-resolution-invalid", "branch-sync-verification-failed", "branch-sync-catalog-incompatible", "sdd-draft-size-exceeded", "sdd-catalog-unsupported-profile", "sdd-catalog-format-invalid", "sdd-catalog-missing-trusted-source", "sdd-draft-format-invalid", "sdd-draft-case-identity-invalid", "sdd-draft-case-budget-invalid", "sdd-draft-requirement-mapping-invalid", "sdd-draft-repository-path-invalid", "deployment-publication-context-missing", "workspace-changes-rejected", "closed-issue-reconciliation-failed", "approval-evidence-read-failed", "change-size-reconciliation-failed", "issue-workflow-admission-blocked", "progress-binary-evidence-unavailable", "progress-result-contract-invalid", "stored-configuration-schema-missing", "stored-configuration-schema-unsupported", "stored-configuration-invalid"]; export type ApplicationErrorRecoveryId = typeof APPLICATION_ERROR_RECOVERY_IDS[number]; interface ApplicationErrorRecoveryVariables extends Readonly>; readonly 'sdd-catalog-unsupported-profile': Readonly>; readonly 'sdd-catalog-format-invalid': Readonly>; + readonly 'sdd-catalog-missing-trusted-source': Readonly>; readonly 'sdd-draft-format-invalid': Readonly>; readonly 'sdd-draft-case-identity-invalid': Readonly>; readonly 'sdd-draft-case-budget-invalid': Readonly>; diff --git a/build/cli/index.js b/build/cli/index.js index 7561c7b62..181fa067e 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -39793,6 +39793,11 @@ const ENGLISH_RECOVERY_CONTENT = Object.freeze({ action: 'Review catalog profile support in the SDD guide. Preserve the native schema, readiness, requirements, evidence and blockers; have a maintainer provide a reviewed compatible adapter, then retry this same issue.', retainedState: 'The repository catalog, answers and published SDDs remain unchanged; no specification or branch was published by this attempt.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'SDD preparation stopped before model analysis because specs/catalog.json is missing from the trusted source revision.', + action: 'Review and merge the catalog prerequisite through the enabled documentation workflow. Update the displayed trusted source through the normal managed-branch workflow, then retry this same issue after the source changes. An unchanged revision remains blocked.', + retainedState: 'Existing answers, published specifications and any linked branch are retained; this attempt published no specification or branch.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'SDD preparation stopped before ownership operations because the catalog cannot establish one valid supported profile and unique owner paths.', action: 'Validate the catalog JSON, supported profile, required metadata and normalized unique owner-path arrays. Preserve repository-owned readiness rules and retry the same issue after maintainer review.', @@ -39951,6 +39956,11 @@ const SPANISH_RECOVERY_CONTENT = Object.freeze({ action: 'Revisa los perfiles admitidos en la guía de SDD. Conserva el esquema nativo, los estados, requisitos, evidencias y bloqueos; pide a un mantenedor un adaptador compatible revisado y reintenta esta misma incidencia.', retainedState: 'El catálogo, las respuestas y los SDD publicados permanecen intactos; este intento no publicó ninguna especificación ni rama.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'La preparación del SDD se detuvo antes del análisis del modelo porque falta specs/catalog.json en la revisión de origen confiable.', + action: 'Revisa e integra el catálogo requerido mediante el workflow de documentación habilitado. Actualiza el origen confiable mostrado mediante el flujo normal de ramas gestionadas y reintenta esta misma incidencia cuando cambie esa revisión. La revisión sin cambios permanece bloqueada.', + retainedState: 'Se conservan las respuestas, las especificaciones publicadas y cualquier rama enlazada; este intento no publicó ninguna especificación ni rama.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'La preparación del SDD se detuvo antes de las operaciones de propiedad porque el catálogo no permite establecer un único perfil admitido con rutas de propiedad válidas y únicas.', action: 'Valida el JSON del catálogo, el perfil admitido, los metadatos obligatorios y las listas de rutas de propiedad normalizadas y únicas. Conserva las reglas de preparación del repositorio y reintenta la misma incidencia tras la revisión de un mantenedor.', @@ -48751,6 +48761,8 @@ const sdd_validation_diagnostic_1 = __nccwpck_require__(34211); const message_catalog_1 = __nccwpck_require__(27097); const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); const UI = Object.freeze({ + missingCatalogSource: ['Trusted source revision', 'Revisión de origen confiable'], + missingCatalogFact: ['Missing prerequisite: specs/catalog.json.', 'Requisito ausente: specs/catalog.json.'], catalogCompatibility: ['Catalog compatibility', 'Compatibilidad del catálogo'], catalogProfile: ['The installed automated writer supports the SDK version-1 profile with specs arrays. A repository-selected schema requires a reviewed compatible adapter.', 'El publicador automático instalado admite el perfil SDK de versión 1 con listas specs. Un esquema seleccionado por el repositorio requiere un adaptador compatible revisado.'], catalogRetained: ['Native schema, readiness, requirements, design and implementation evidence, and blockers remain unchanged. Review the supported-profile boundary before retrying this same issue.', 'El esquema nativo, los estados, requisitos, evidencias de diseño e implementación y bloqueos permanecen intactos. Revisa los perfiles admitidos antes de reintentar esta misma incidencia.'], @@ -48883,6 +48895,11 @@ function renderSddPreparationFailure(options, catalog = (0, sdd_preparation_mess || typeof options.error.retryable !== 'boolean' || !(0, application_error_1.isApplicationErrorCorrelationId)(options.error.correlationId)) { throw new TypeError('Invalid SDD status identity or semantic error facts.'); } + const missingCatalogSource = options.missingCatalogSource; + if (missingCatalogSource !== undefined && (typeof missingCatalogSource !== 'string' + || !/^[a-f0-9]{40}$/u.test(missingCatalogSource) || options.catalogReason !== undefined || options.diagnostic !== undefined)) { + throw new TypeError('Invalid exclusive missing trusted-catalog facts.'); + } if (options.catalogReason !== undefined && (!sdd_catalog_compatibility_1.SDD_CATALOG_COMPATIBILITY_REASONS.includes(options.catalogReason) || options.diagnostic)) { throw new TypeError('Invalid exclusive catalog compatibility facts.'); } @@ -48908,7 +48925,10 @@ function renderSddPreparationFailure(options, catalog = (0, sdd_preparation_mess `**${message('sddPreparation.code')}:** \`${view.code}\``, `**${message('sddPreparation.reference')}:** \`${view.reference}\``, ]; - if (options.catalogReason) { + if (missingCatalogSource) { + lines.push(`**${message('sddPreparation.missingCatalogSource')}:** \`${missingCatalogSource}\``, '', message('sddPreparation.missingCatalogFact')); + } + else if (options.catalogReason) { lines.push(`**${message('sddPreparation.catalogCompatibility')}:** \`${options.catalogReason}\``, '', message('sddPreparation.catalogProfile'), '', message('sddPreparation.catalogRetained')); } else if (diagnostic) { @@ -63648,6 +63668,7 @@ async function draftSddMarkdown(agent, configuration, plan, prompt, validate) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PreBranchSddGateUseCase = void 0; const sdd_catalog_compatibility_1 = __nccwpck_require__(95312); +const sdd_trusted_catalog_failure_1 = __nccwpck_require__(44763); const sdd_draft_preparation_failure_1 = __nccwpck_require__(12321); const sdd_preparation_status_policy_1 = __nccwpck_require__(78345); const sdd_preparation_message_catalog_1 = __nccwpck_require__(88171); @@ -63847,7 +63868,7 @@ class PreBranchSddGateUseCase { return { status: 'drafted', prepared, record, ...(card ? { cardId: card.id } : {}), results: [this.result(true, true, `Validated ${plan.path} before branch publication.`)] }; } catch (error) { - if (error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure && error.sourceCommit) { + if ((error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure || error instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure) && error.sourceCommit) { failureSource = { branch: failureBranch, sha: error.sourceCommit }; } const failure = this.failure(error); @@ -63961,8 +63982,8 @@ class PreBranchSddGateUseCase { async writePreparationFailure(context, error, cause, publicationPending, source) { if (!context.tokenUser.trim()) throw new application_error_1.ApplicationError('configuration.unsupported', 'The SDD diagnostic card requires a trusted bot identity.'); - if (cause instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure && !source) - throw new application_error_1.ApplicationError('configuration.unsupported', 'The catalog compatibility diagnostic requires a verified source revision.'); + if ((cause instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure || cause instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure) && !source) + throw new application_error_1.ApplicationError('configuration.unsupported', 'The catalog diagnostic requires a verified source revision.'); const comments = await this.comments.listIssueComments(context.issueNumber); const [catalog, errorCatalog] = await Promise.all([ (0, sdd_preparation_message_catalog_1.resolveSddPreparationCatalog)(context.issueLocale ?? 'en-US', context.agentConfiguration, this.presentation.catalogResolver), @@ -63973,6 +63994,7 @@ class PreBranchSddGateUseCase { error, publicationPending, run: this.presentation.run, ...(cause instanceof sdd_draft_preparation_failure_1.SddDraftPreparationFailure ? { diagnostic: cause.diagnostic, attempts: cause.attempts } : {}), ...(cause instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure ? { catalogReason: cause.reason } : {}), + ...(cause instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure ? { missingCatalogSource: cause.sourceCommit } : {}), }, aligned ? catalog : (0, sdd_preparation_message_catalog_1.resolveStaticSddPreparationCatalog)('en-US'), aligned ? errorCatalog.message : (0, application_error_message_catalog_1.resolveStaticApplicationErrorCatalog)('en-US').message) + (aligned ? '' : '\n\n' + (0, sdd_preparation_message_catalog_1.resolveStaticSddPreparationCatalog)('en-US').message('sddPreparation.fallbackSlices')); if (source) @@ -64029,7 +64051,10 @@ class PreBranchSddGateUseCase { failure(error) { // The runtime logger redacts credentials and makes workflow-command text inert. (0, logging_ports_1.logError)(`Pre-branch SDD gate failed: ${error instanceof Error ? error.message : 'unknown failure'}`); - const semanticError = error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure + const semanticError = error instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure + ? new application_error_1.ApplicationError('configuration.unsupported', error.message, { recovery: { + id: 'sdd-catalog-missing-trusted-source', variables: {}, + } }) : error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure ? new application_error_1.ApplicationError('configuration.unsupported', error.message, { recovery: { id: error.reason === 'unsupported-profile' ? 'sdd-catalog-unsupported-profile' : 'sdd-catalog-format-invalid', variables: {}, } }) : (0, application_error_1.toApplicationError)(error, 'workflow.failed', 'The pre-branch SDD gate is blocked.'); @@ -81811,6 +81836,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-catalog-unsupported-profile', 'sdd-catalog-format-invalid', + 'sdd-catalog-missing-trusted-source', 'sdd-draft-format-invalid', 'sdd-draft-case-identity-invalid', 'sdd-draft-case-budget-invalid', @@ -82057,6 +82083,7 @@ const RECOVERY_VARIABLE_KEYS = Object.freeze({ 'sdd-draft-size-exceeded': Object.freeze([]), 'sdd-catalog-unsupported-profile': Object.freeze([]), 'sdd-catalog-format-invalid': Object.freeze([]), + 'sdd-catalog-missing-trusted-source': Object.freeze([]), 'sdd-draft-format-invalid': Object.freeze([]), 'sdd-draft-case-identity-invalid': Object.freeze([]), 'sdd-draft-case-budget-invalid': Object.freeze([]), @@ -99071,6 +99098,30 @@ function uniqueIdentifiers(values, pattern, reason, section) { } +/***/ }), + +/***/ 44763: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddTrustedCatalogMissingFailure = void 0; +/** Closed prerequisite fact; never carries provider stderr or candidate content. */ +class SddTrustedCatalogMissingFailure extends Error { + constructor(sourceCommit) { + if (typeof sourceCommit !== 'string' || !/^[a-f0-9]{40}$/iu.test(sourceCommit)) { + throw new TypeError('Invalid trusted SDD catalog source revision.'); + } + super('The trusted source does not contain specs/catalog.json.'); + this.name = 'SddTrustedCatalogMissingFailure'; + this.sourceCommit = sourceCommit.toLowerCase(); + Object.freeze(this); + } +} +exports.SddTrustedCatalogMissingFailure = SddTrustedCatalogMissingFailure; + + /***/ }), /***/ 34211: @@ -104914,6 +104965,7 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PreBranchSddWorkspaceAdapter = void 0; const sdd_catalog_compatibility_1 = __nccwpck_require__(95312); +const sdd_trusted_catalog_failure_1 = __nccwpck_require__(44763); const fs = __importStar(__nccwpck_require__(87561)); const os = __importStar(__nccwpck_require__(70612)); const path = __importStar(__nccwpck_require__(49411)); @@ -104951,6 +105003,10 @@ class PreBranchSddWorkspaceAdapter { return this.readSnapshotAtSha(commitSha); } async readSnapshotAtSha(baseSha) { + // A successful immutable lookup proves absence; object/read failures remain Git failures. + const catalogEntry = await this.git(['ls-tree', '-z', '--full-tree', `${baseSha}^{commit}`, '--', 'specs/catalog.json']); + if (catalogEntry === '') + throw new sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure(baseSha); const raw = await this.git(['show', `${baseSha}:specs/catalog.json`]); let catalog; try { diff --git a/build/deployment_recovery/index.js b/build/deployment_recovery/index.js index 689beadaf..05d2abf8f 100644 --- a/build/deployment_recovery/index.js +++ b/build/deployment_recovery/index.js @@ -32606,6 +32606,11 @@ const ENGLISH_RECOVERY_CONTENT = Object.freeze({ action: 'Review catalog profile support in the SDD guide. Preserve the native schema, readiness, requirements, evidence and blockers; have a maintainer provide a reviewed compatible adapter, then retry this same issue.', retainedState: 'The repository catalog, answers and published SDDs remain unchanged; no specification or branch was published by this attempt.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'SDD preparation stopped before model analysis because specs/catalog.json is missing from the trusted source revision.', + action: 'Review and merge the catalog prerequisite through the enabled documentation workflow. Update the displayed trusted source through the normal managed-branch workflow, then retry this same issue after the source changes. An unchanged revision remains blocked.', + retainedState: 'Existing answers, published specifications and any linked branch are retained; this attempt published no specification or branch.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'SDD preparation stopped before ownership operations because the catalog cannot establish one valid supported profile and unique owner paths.', action: 'Validate the catalog JSON, supported profile, required metadata and normalized unique owner-path arrays. Preserve repository-owned readiness rules and retry the same issue after maintainer review.', @@ -32764,6 +32769,11 @@ const SPANISH_RECOVERY_CONTENT = Object.freeze({ action: 'Revisa los perfiles admitidos en la guía de SDD. Conserva el esquema nativo, los estados, requisitos, evidencias y bloqueos; pide a un mantenedor un adaptador compatible revisado y reintenta esta misma incidencia.', retainedState: 'El catálogo, las respuestas y los SDD publicados permanecen intactos; este intento no publicó ninguna especificación ni rama.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'La preparación del SDD se detuvo antes del análisis del modelo porque falta specs/catalog.json en la revisión de origen confiable.', + action: 'Revisa e integra el catálogo requerido mediante el workflow de documentación habilitado. Actualiza el origen confiable mostrado mediante el flujo normal de ramas gestionadas y reintenta esta misma incidencia cuando cambie esa revisión. La revisión sin cambios permanece bloqueada.', + retainedState: 'Se conservan las respuestas, las especificaciones publicadas y cualquier rama enlazada; este intento no publicó ninguna especificación ni rama.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'La preparación del SDD se detuvo antes de las operaciones de propiedad porque el catálogo no permite establecer un único perfil admitido con rutas de propiedad válidas y únicas.', action: 'Valida el JSON del catálogo, el perfil admitido, los metadatos obligatorios y las listas de rutas de propiedad normalizadas y únicas. Conserva las reglas de preparación del repositorio y reintenta la misma incidencia tras la revisión de un mantenedor.', @@ -33703,6 +33713,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-catalog-unsupported-profile', 'sdd-catalog-format-invalid', + 'sdd-catalog-missing-trusted-source', 'sdd-draft-format-invalid', 'sdd-draft-case-identity-invalid', 'sdd-draft-case-budget-invalid', @@ -33949,6 +33960,7 @@ const RECOVERY_VARIABLE_KEYS = Object.freeze({ 'sdd-draft-size-exceeded': Object.freeze([]), 'sdd-catalog-unsupported-profile': Object.freeze([]), 'sdd-catalog-format-invalid': Object.freeze([]), + 'sdd-catalog-missing-trusted-source': Object.freeze([]), 'sdd-draft-format-invalid': Object.freeze([]), 'sdd-draft-case-identity-invalid': Object.freeze([]), 'sdd-draft-case-budget-invalid': Object.freeze([]), diff --git a/build/github_action/index.js b/build/github_action/index.js index 76cc17820..a68969d8b 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -41370,6 +41370,11 @@ const ENGLISH_RECOVERY_CONTENT = Object.freeze({ action: 'Review catalog profile support in the SDD guide. Preserve the native schema, readiness, requirements, evidence and blockers; have a maintainer provide a reviewed compatible adapter, then retry this same issue.', retainedState: 'The repository catalog, answers and published SDDs remain unchanged; no specification or branch was published by this attempt.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'SDD preparation stopped before model analysis because specs/catalog.json is missing from the trusted source revision.', + action: 'Review and merge the catalog prerequisite through the enabled documentation workflow. Update the displayed trusted source through the normal managed-branch workflow, then retry this same issue after the source changes. An unchanged revision remains blocked.', + retainedState: 'Existing answers, published specifications and any linked branch are retained; this attempt published no specification or branch.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'SDD preparation stopped before ownership operations because the catalog cannot establish one valid supported profile and unique owner paths.', action: 'Validate the catalog JSON, supported profile, required metadata and normalized unique owner-path arrays. Preserve repository-owned readiness rules and retry the same issue after maintainer review.', @@ -41528,6 +41533,11 @@ const SPANISH_RECOVERY_CONTENT = Object.freeze({ action: 'Revisa los perfiles admitidos en la guía de SDD. Conserva el esquema nativo, los estados, requisitos, evidencias y bloqueos; pide a un mantenedor un adaptador compatible revisado y reintenta esta misma incidencia.', retainedState: 'El catálogo, las respuestas y los SDD publicados permanecen intactos; este intento no publicó ninguna especificación ni rama.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'La preparación del SDD se detuvo antes del análisis del modelo porque falta specs/catalog.json en la revisión de origen confiable.', + action: 'Revisa e integra el catálogo requerido mediante el workflow de documentación habilitado. Actualiza el origen confiable mostrado mediante el flujo normal de ramas gestionadas y reintenta esta misma incidencia cuando cambie esa revisión. La revisión sin cambios permanece bloqueada.', + retainedState: 'Se conservan las respuestas, las especificaciones publicadas y cualquier rama enlazada; este intento no publicó ninguna especificación ni rama.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'La preparación del SDD se detuvo antes de las operaciones de propiedad porque el catálogo no permite establecer un único perfil admitido con rutas de propiedad válidas y únicas.', action: 'Valida el JSON del catálogo, el perfil admitido, los metadatos obligatorios y las listas de rutas de propiedad normalizadas y únicas. Conserva las reglas de preparación del repositorio y reintenta la misma incidencia tras la revisión de un mantenedor.', @@ -50610,6 +50620,8 @@ const sdd_validation_diagnostic_1 = __nccwpck_require__(34211); const message_catalog_1 = __nccwpck_require__(27097); const resolved_message_catalog_policy_1 = __nccwpck_require__(55069); const UI = Object.freeze({ + missingCatalogSource: ['Trusted source revision', 'Revisión de origen confiable'], + missingCatalogFact: ['Missing prerequisite: specs/catalog.json.', 'Requisito ausente: specs/catalog.json.'], catalogCompatibility: ['Catalog compatibility', 'Compatibilidad del catálogo'], catalogProfile: ['The installed automated writer supports the SDK version-1 profile with specs arrays. A repository-selected schema requires a reviewed compatible adapter.', 'El publicador automático instalado admite el perfil SDK de versión 1 con listas specs. Un esquema seleccionado por el repositorio requiere un adaptador compatible revisado.'], catalogRetained: ['Native schema, readiness, requirements, design and implementation evidence, and blockers remain unchanged. Review the supported-profile boundary before retrying this same issue.', 'El esquema nativo, los estados, requisitos, evidencias de diseño e implementación y bloqueos permanecen intactos. Revisa los perfiles admitidos antes de reintentar esta misma incidencia.'], @@ -50742,6 +50754,11 @@ function renderSddPreparationFailure(options, catalog = (0, sdd_preparation_mess || typeof options.error.retryable !== 'boolean' || !(0, application_error_1.isApplicationErrorCorrelationId)(options.error.correlationId)) { throw new TypeError('Invalid SDD status identity or semantic error facts.'); } + const missingCatalogSource = options.missingCatalogSource; + if (missingCatalogSource !== undefined && (typeof missingCatalogSource !== 'string' + || !/^[a-f0-9]{40}$/u.test(missingCatalogSource) || options.catalogReason !== undefined || options.diagnostic !== undefined)) { + throw new TypeError('Invalid exclusive missing trusted-catalog facts.'); + } if (options.catalogReason !== undefined && (!sdd_catalog_compatibility_1.SDD_CATALOG_COMPATIBILITY_REASONS.includes(options.catalogReason) || options.diagnostic)) { throw new TypeError('Invalid exclusive catalog compatibility facts.'); } @@ -50767,7 +50784,10 @@ function renderSddPreparationFailure(options, catalog = (0, sdd_preparation_mess `**${message('sddPreparation.code')}:** \`${view.code}\``, `**${message('sddPreparation.reference')}:** \`${view.reference}\``, ]; - if (options.catalogReason) { + if (missingCatalogSource) { + lines.push(`**${message('sddPreparation.missingCatalogSource')}:** \`${missingCatalogSource}\``, '', message('sddPreparation.missingCatalogFact')); + } + else if (options.catalogReason) { lines.push(`**${message('sddPreparation.catalogCompatibility')}:** \`${options.catalogReason}\``, '', message('sddPreparation.catalogProfile'), '', message('sddPreparation.catalogRetained')); } else if (diagnostic) { @@ -62056,6 +62076,7 @@ async function draftSddMarkdown(agent, configuration, plan, prompt, validate) { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PreBranchSddGateUseCase = void 0; const sdd_catalog_compatibility_1 = __nccwpck_require__(95312); +const sdd_trusted_catalog_failure_1 = __nccwpck_require__(44763); const sdd_draft_preparation_failure_1 = __nccwpck_require__(12321); const sdd_preparation_status_policy_1 = __nccwpck_require__(78345); const sdd_preparation_message_catalog_1 = __nccwpck_require__(88171); @@ -62255,7 +62276,7 @@ class PreBranchSddGateUseCase { return { status: 'drafted', prepared, record, ...(card ? { cardId: card.id } : {}), results: [this.result(true, true, `Validated ${plan.path} before branch publication.`)] }; } catch (error) { - if (error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure && error.sourceCommit) { + if ((error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure || error instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure) && error.sourceCommit) { failureSource = { branch: failureBranch, sha: error.sourceCommit }; } const failure = this.failure(error); @@ -62369,8 +62390,8 @@ class PreBranchSddGateUseCase { async writePreparationFailure(context, error, cause, publicationPending, source) { if (!context.tokenUser.trim()) throw new application_error_1.ApplicationError('configuration.unsupported', 'The SDD diagnostic card requires a trusted bot identity.'); - if (cause instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure && !source) - throw new application_error_1.ApplicationError('configuration.unsupported', 'The catalog compatibility diagnostic requires a verified source revision.'); + if ((cause instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure || cause instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure) && !source) + throw new application_error_1.ApplicationError('configuration.unsupported', 'The catalog diagnostic requires a verified source revision.'); const comments = await this.comments.listIssueComments(context.issueNumber); const [catalog, errorCatalog] = await Promise.all([ (0, sdd_preparation_message_catalog_1.resolveSddPreparationCatalog)(context.issueLocale ?? 'en-US', context.agentConfiguration, this.presentation.catalogResolver), @@ -62381,6 +62402,7 @@ class PreBranchSddGateUseCase { error, publicationPending, run: this.presentation.run, ...(cause instanceof sdd_draft_preparation_failure_1.SddDraftPreparationFailure ? { diagnostic: cause.diagnostic, attempts: cause.attempts } : {}), ...(cause instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure ? { catalogReason: cause.reason } : {}), + ...(cause instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure ? { missingCatalogSource: cause.sourceCommit } : {}), }, aligned ? catalog : (0, sdd_preparation_message_catalog_1.resolveStaticSddPreparationCatalog)('en-US'), aligned ? errorCatalog.message : (0, application_error_message_catalog_1.resolveStaticApplicationErrorCatalog)('en-US').message) + (aligned ? '' : '\n\n' + (0, sdd_preparation_message_catalog_1.resolveStaticSddPreparationCatalog)('en-US').message('sddPreparation.fallbackSlices')); if (source) @@ -62437,7 +62459,10 @@ class PreBranchSddGateUseCase { failure(error) { // The runtime logger redacts credentials and makes workflow-command text inert. (0, logging_ports_1.logError)(`Pre-branch SDD gate failed: ${error instanceof Error ? error.message : 'unknown failure'}`); - const semanticError = error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure + const semanticError = error instanceof sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure + ? new application_error_1.ApplicationError('configuration.unsupported', error.message, { recovery: { + id: 'sdd-catalog-missing-trusted-source', variables: {}, + } }) : error instanceof sdd_catalog_compatibility_1.SddCatalogCompatibilityFailure ? new application_error_1.ApplicationError('configuration.unsupported', error.message, { recovery: { id: error.reason === 'unsupported-profile' ? 'sdd-catalog-unsupported-profile' : 'sdd-catalog-format-invalid', variables: {}, } }) : (0, application_error_1.toApplicationError)(error, 'workflow.failed', 'The pre-branch SDD gate is blocked.'); @@ -72592,6 +72617,7 @@ exports.APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-catalog-unsupported-profile', 'sdd-catalog-format-invalid', + 'sdd-catalog-missing-trusted-source', 'sdd-draft-format-invalid', 'sdd-draft-case-identity-invalid', 'sdd-draft-case-budget-invalid', @@ -72838,6 +72864,7 @@ const RECOVERY_VARIABLE_KEYS = Object.freeze({ 'sdd-draft-size-exceeded': Object.freeze([]), 'sdd-catalog-unsupported-profile': Object.freeze([]), 'sdd-catalog-format-invalid': Object.freeze([]), + 'sdd-catalog-missing-trusted-source': Object.freeze([]), 'sdd-draft-format-invalid': Object.freeze([]), 'sdd-draft-case-identity-invalid': Object.freeze([]), 'sdd-draft-case-budget-invalid': Object.freeze([]), @@ -90071,6 +90098,30 @@ function uniqueIdentifiers(values, pattern, reason, section) { } +/***/ }), + +/***/ 44763: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SddTrustedCatalogMissingFailure = void 0; +/** Closed prerequisite fact; never carries provider stderr or candidate content. */ +class SddTrustedCatalogMissingFailure extends Error { + constructor(sourceCommit) { + if (typeof sourceCommit !== 'string' || !/^[a-f0-9]{40}$/iu.test(sourceCommit)) { + throw new TypeError('Invalid trusted SDD catalog source revision.'); + } + super('The trusted source does not contain specs/catalog.json.'); + this.name = 'SddTrustedCatalogMissingFailure'; + this.sourceCommit = sourceCommit.toLowerCase(); + Object.freeze(this); + } +} +exports.SddTrustedCatalogMissingFailure = SddTrustedCatalogMissingFailure; + + /***/ }), /***/ 34211: @@ -95993,6 +96044,7 @@ var __importStar = (this && this.__importStar) || (function () { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.PreBranchSddWorkspaceAdapter = void 0; const sdd_catalog_compatibility_1 = __nccwpck_require__(95312); +const sdd_trusted_catalog_failure_1 = __nccwpck_require__(44763); const fs = __importStar(__nccwpck_require__(87561)); const os = __importStar(__nccwpck_require__(70612)); const path = __importStar(__nccwpck_require__(49411)); @@ -96030,6 +96082,10 @@ class PreBranchSddWorkspaceAdapter { return this.readSnapshotAtSha(commitSha); } async readSnapshotAtSha(baseSha) { + // A successful immutable lookup proves absence; object/read failures remain Git failures. + const catalogEntry = await this.git(['ls-tree', '-z', '--full-tree', `${baseSha}^{commit}`, '--', 'specs/catalog.json']); + if (catalogEntry === '') + throw new sdd_trusted_catalog_failure_1.SddTrustedCatalogMissingFailure(baseSha); const raw = await this.git(['show', `${baseSha}:specs/catalog.json`]); let catalog; try { diff --git a/docs/issues/pre-branch-sdds.mdx b/docs/issues/pre-branch-sdds.mdx index 105e593fb..3738d31e7 100644 --- a/docs/issues/pre-branch-sdds.mdx +++ b/docs/issues/pre-branch-sdds.mdx @@ -192,6 +192,32 @@ Retain the prospective documents while that upgrade is pending. Keep the repository's native schema and readiness rules if it uses another profile; the compatibility recovery below still applies. +## Missing catalog in the trusted source + +When `specs/catalog.json` is absent from the pinned trusted revision, preparation +stops before model analysis and branch or specification publication. The blocked +card names the missing prerequisite and shows the immutable source SHA in its +technical details. A catalog copied into the candidate workspace or an unmerged +documentation PR does not change that revision. + +Review and merge the prerequisite through the enabled documentation workflow. +Update the displayed trusted source through the normal managed-branch workflow, +then retry the same issue after that source changes. An existing linked branch +uses its own trusted source; advancing development alone may not update it. +Answers, published specifications and retained branch state remain available. +Retrying an unchanged missing source cannot recover it. + +| Observed condition | Result and next step | +|---|---| +| Catalog absent at the trusted commit | Missing prerequisite; review and merge it before retrying | +| Catalog exists but its format/profile is unsupported | Follow catalog publication-profile recovery below | +| Git object/read failure or missing template/standard | Resolve that distinct read/prerequisite failure; it is not proof the catalog is absent | + +The Action checks source and issue freshness before replacing its owned card. A +moved or unavailable source preserves newer status. Once a reviewed catalog +reaches the trusted revision, normal profile validation, clarification and +publication checks still apply; catalog presence alone does not grant readiness. + ## Catalog publication profiles The installed automatic writer supports the **SDK version-1** catalog profile: diff --git a/docs/security-operations/operations/error-reference.mdx b/docs/security-operations/operations/error-reference.mdx index dfa2b380f..0970daa27 100644 --- a/docs/security-operations/operations/error-reference.mdx +++ b/docs/security-operations/operations/error-reference.mdx @@ -81,6 +81,14 @@ before retrying the same issue. The diagnostic preserves repository-native schema/readiness, answers and historical publication; a rejected profile does not publish a branch or start the model. +Confirmed catalog absence at the trusted revision uses `configuration.unsupported` +with `sdd-catalog-missing-trusted-source`. The owned preparation card identifies +`specs/catalog.json` and its immutable source SHA. Review and merge the prerequisite, +update the displayed trusted source through the normal branch workflow, and retry +the same issue after that source changes. Existing answers, specifications and any +linked branch are retained. A Git object/read failure keeps its distinct error +classification. See [missing trusted catalog recovery](/issues/pre-branch-sdds#missing-catalog-in-the-trusted-source). + For progress, a unique exact same-repository PR owns the comparison base; only proven absence uses development. Inherited production libraries can therefore be unchanged in the real PR delta. If binary/non-text changes remain unsupported, diff --git a/specs/CATALOG.md b/specs/CATALOG.md index 265d4f5cf..ebf922456 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) + 2 companion | 248 paths · 2026-10-21 | | `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) | 36 paths · 2026-10-18 | -| `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 and preserve complete navigable catalog links for accepted repository filenames and safely publish generated catalogs without following destination links; bootstrap truthful proposed SDK capabilities before implementation without changing readiness; reject incomplete specification discovery explicitly without following unregistered symbolic links | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 140 paths · 2026-10-30 | +| `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 and preserve complete navigable catalog links for accepted repository filenames and safely publish generated catalogs without following destination links; bootstrap truthful proposed SDK capabilities before implementation without changing readiness; reject incomplete specification discovery explicitly without following unregistered symbolic links; report confirmed missing trusted catalogs with immutable source provenance and prerequisite recovery before model or publication | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 142 paths · 2026-10-30 | | `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) | 75 paths · 2026-10-30 | | `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) | 88 paths · 2026-10-26 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze bounded diff partitions with version-grounded dependency, policy-to-request transformation and source-bound assertion and generated-source coordinates with independent candidate verification, configuration-selected initial-session credential ownership, delegated test-path and external Action input-contract, host-aware self-repository references and Kotlin/Wasm lexical-binding and renderer-grounded Markdown path evidence, publish stable findings atomically, and apply authorized verified fixes with regression coverage for cwd/pattern-grounded indirect unittest discovery and source-bound module execution evidence, with active-instance Kotlin Json nullable-input policy grounding | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 3 companion | 154 paths · 2026-10-30 | @@ -152,8 +152,8 @@ debt or convert unknown historic intent into a design decision. - 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) · [`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) · [`src/domain/specification_catalog_path_policy.cjs`](../src/domain/specification_catalog_path_policy.cjs) · [`src/application/policies/github_runtime_doctor_policy.ts`](../src/application/policies/github_runtime_doctor_policy.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/domain/sdd_prerequisite_policy.cjs`](../src/domain/sdd_prerequisite_policy.cjs) · [`src/application/policies/sdd_analysis_status_policy.ts`](../src/application/policies/sdd_analysis_status_policy.ts) · [`src/domain/sdd_draft_consistency.ts`](../src/domain/sdd_draft_consistency.ts) · [`src/domain/sdd_validation_diagnostic.ts`](../src/domain/sdd_validation_diagnostic.ts) · [`src/application/errors/sdd_draft_preparation_failure.ts`](../src/application/errors/sdd_draft_preparation_failure.ts) · [`src/application/policies/sdd_preparation_message_catalog.ts`](../src/application/policies/sdd_preparation_message_catalog.ts) · [`src/application/policies/sdd_preparation_status_policy.ts`](../src/application/policies/sdd_preparation_status_policy.ts) · [`src/domain/sdd_catalog_compatibility.ts`](../src/domain/sdd_catalog_compatibility.ts) · [`src/infrastructure/specification_catalog_publication.cjs`](../src/infrastructure/specification_catalog_publication.cjs) · [`jest.config.js`](../jest.config.js) -- 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) · [`src/tooling/__tests__/portable_specification_catalog.test.ts`](../src/tooling/__tests__/portable_specification_catalog.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_policy.test.ts) · [`src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts`](../src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/domain/__tests__/sdd_draft_consistency.test.ts`](../src/domain/__tests__/sdd_draft_consistency.test.ts) · [`src/domain/__tests__/sdd_derived_tables.test.ts`](../src/domain/__tests__/sdd_derived_tables.test.ts) · [`src/domain/__tests__/sdd_validation_diagnostic.test.ts`](../src/domain/__tests__/sdd_validation_diagnostic.test.ts) · [`src/application/policies/__tests__/sdd_preparation_status_policy.test.ts`](../src/application/policies/__tests__/sdd_preparation_status_policy.test.ts) · [`src/domain/__tests__/sdd_catalog_compatibility.test.ts`](../src/domain/__tests__/sdd_catalog_compatibility.test.ts) · [`src/testing/fixtures/sdd-native-catalog/catalog.json`](../src/testing/fixtures/sdd-native-catalog/catalog.json) · [`src/testing/fixtures/sdd-native-catalog/catalog.schema.json`](../src/testing/fixtures/sdd-native-catalog/catalog.schema.json) · [`src/testing/fixtures/sdd-native-catalog/provenance.json`](../src/testing/fixtures/sdd-native-catalog/provenance.json) · [`src/tooling/__tests__/specification_catalog_publication.test.ts`](../src/tooling/__tests__/specification_catalog_publication.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts) · [`src/tooling/__tests__/specification_catalog_markdown_links.test.ts`](../src/tooling/__tests__/specification_catalog_markdown_links.test.ts) · [`src/tooling/__tests__/specification_catalog_discovery.test.ts`](../src/tooling/__tests__/specification_catalog_discovery.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) · [`src/domain/specification_catalog_path_policy.cjs`](../src/domain/specification_catalog_path_policy.cjs) · [`src/application/policies/github_runtime_doctor_policy.ts`](../src/application/policies/github_runtime_doctor_policy.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/domain/sdd_prerequisite_policy.cjs`](../src/domain/sdd_prerequisite_policy.cjs) · [`src/application/policies/sdd_analysis_status_policy.ts`](../src/application/policies/sdd_analysis_status_policy.ts) · [`src/domain/sdd_draft_consistency.ts`](../src/domain/sdd_draft_consistency.ts) · [`src/domain/sdd_validation_diagnostic.ts`](../src/domain/sdd_validation_diagnostic.ts) · [`src/application/errors/sdd_draft_preparation_failure.ts`](../src/application/errors/sdd_draft_preparation_failure.ts) · [`src/application/policies/sdd_preparation_message_catalog.ts`](../src/application/policies/sdd_preparation_message_catalog.ts) · [`src/application/policies/sdd_preparation_status_policy.ts`](../src/application/policies/sdd_preparation_status_policy.ts) · [`src/domain/sdd_catalog_compatibility.ts`](../src/domain/sdd_catalog_compatibility.ts) · [`src/infrastructure/specification_catalog_publication.cjs`](../src/infrastructure/specification_catalog_publication.cjs) · [`jest.config.js`](../jest.config.js) · [`src/domain/sdd_trusted_catalog_failure.ts`](../src/domain/sdd_trusted_catalog_failure.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/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) · [`src/tooling/__tests__/portable_specification_catalog.test.ts`](../src/tooling/__tests__/portable_specification_catalog.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_policy.test.ts) · [`src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts`](../src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/domain/__tests__/sdd_draft_consistency.test.ts`](../src/domain/__tests__/sdd_draft_consistency.test.ts) · [`src/domain/__tests__/sdd_derived_tables.test.ts`](../src/domain/__tests__/sdd_derived_tables.test.ts) · [`src/domain/__tests__/sdd_validation_diagnostic.test.ts`](../src/domain/__tests__/sdd_validation_diagnostic.test.ts) · [`src/application/policies/__tests__/sdd_preparation_status_policy.test.ts`](../src/application/policies/__tests__/sdd_preparation_status_policy.test.ts) · [`src/domain/__tests__/sdd_catalog_compatibility.test.ts`](../src/domain/__tests__/sdd_catalog_compatibility.test.ts) · [`src/testing/fixtures/sdd-native-catalog/catalog.json`](../src/testing/fixtures/sdd-native-catalog/catalog.json) · [`src/testing/fixtures/sdd-native-catalog/catalog.schema.json`](../src/testing/fixtures/sdd-native-catalog/catalog.schema.json) · [`src/testing/fixtures/sdd-native-catalog/provenance.json`](../src/testing/fixtures/sdd-native-catalog/provenance.json) · [`src/tooling/__tests__/specification_catalog_publication.test.ts`](../src/tooling/__tests__/specification_catalog_publication.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts) · [`src/tooling/__tests__/specification_catalog_markdown_links.test.ts`](../src/tooling/__tests__/specification_catalog_markdown_links.test.ts) · [`src/tooling/__tests__/specification_catalog_discovery.test.ts`](../src/tooling/__tests__/specification_catalog_discovery.test.ts) · [`src/domain/__tests__/sdd_trusted_catalog_failure.test.ts`](../src/domain/__tests__/sdd_trusted_catalog_failure.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) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) · [`specs/README.md`](./README.md) ### `managed-issue-lifecycle` — Managed issue and branch lifecycle diff --git a/specs/catalog.json b/specs/catalog.json index 559529b0a..e8567eec4 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1685,7 +1685,7 @@ "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; inspect verified SDD status and bounded structural publication comparisons and preserve complete navigable catalog links for accepted repository filenames and safely publish generated catalogs without following destination links; bootstrap truthful proposed SDK capabilities before implementation without changing readiness; reject incomplete specification discovery explicitly without following unregistered symbolic links", + "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 and preserve complete navigable catalog links for accepted repository filenames and safely publish generated catalogs without following destination links; bootstrap truthful proposed SDK capabilities before implementation without changing readiness; reject incomplete specification discovery explicitly without following unregistered symbolic links; report confirmed missing trusted catalogs with immutable source provenance and prerequisite recovery before model or publication", "owner": "Copilot maintainers", "lastVerified": "2026-10-30", "specs": [ @@ -1772,7 +1772,8 @@ "src/application/policies/sdd_preparation_status_policy.ts", "src/domain/sdd_catalog_compatibility.ts", "src/infrastructure/specification_catalog_publication.cjs", - "jest.config.js" + "jest.config.js", + "src/domain/sdd_trusted_catalog_failure.ts" ], "tests": [ "src/domain/__tests__/issue_workflow_runtime_policy.test.ts", @@ -1828,7 +1829,8 @@ "src/tooling/__tests__/proposed_sdd_catalog.test.ts", "src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts", "src/tooling/__tests__/specification_catalog_markdown_links.test.ts", - "src/tooling/__tests__/specification_catalog_discovery.test.ts" + "src/tooling/__tests__/specification_catalog_discovery.test.ts", + "src/domain/__tests__/sdd_trusted_catalog_failure.test.ts" ], "documentation": [ "docs/issues/branch-management.mdx", diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index f4fcecd7a..c5c63a96c 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -3,7 +3,7 @@ - Status: Implemented — automated verification complete; live provider UX review pending - Date: 2026-09-17 - Catalog capability ID: `issue-start-and-sdd-readiness` -- Last verified: 2026-10-30 (complete no-follow specification discovery, #804; navigable accepted catalog filenames, #782; truthful proposed SDK bootstrap, #777; safe portable catalog publication, #771; nested catalog links, #768; actionable blocked preparation and safe diagnostics, #714) +- Last verified: 2026-10-30 (missing trusted-catalog prerequisite, #808; complete no-follow specification discovery, #804; navigable accepted catalog filenames, #782; truthful proposed SDK bootstrap, #777; safe portable catalog publication, #771; nested catalog links, #768; actionable blocked preparation and safe diagnostics, #714) - Owners: Copilot maintainers - Scope: clarify, update, validate, and publish the owning SDD before eligible branch-based work - Related issues/PRs: none; local design work; companion SDD `issue-start-and-branch-readiness.md` @@ -2028,7 +2028,8 @@ again: | SDD planning and publication | 6 | New proposed entry, rejected promotion, update/companion/new scratch writes and metadata retention | | Generator/validator entrypoint | 4 | Generate, read-only validation, stale output and invalid evidence without false success | | Generated UX and documentation | 4 | Explicit not-implemented state, retained workflow semantics, populated links and executable bootstrap guidance | -| **Total** | **34** | Deterministic, owned fixtures; no consumer mutations or claimed live application acceptance | +| Recovery registry | 1 | Required closed-variant matrix covers the new EN/ES retained-state contract | +| **Total** | **35** | Deterministic, owned fixtures; no consumer mutations or claimed live application acceptance | Collect the previously unmeasured CJS path policy and catalog validator in the mandatory root coverage run. Enforce 100% statements/branches/functions/lines @@ -2257,3 +2258,111 @@ or branches intersecting added source lines. Four related doctor, branch-sync and workspace suites pass 115 tests with one existing platform skip. Fresh full-hook and remote gates remain required; these owned fixtures do not claim consumer application execution or adoption. + +### Missing catalog in the trusted revision — #808 (2026-10-30) + +The existing Git adapter reads the pinned catalog before its typed profile +validation. An absent catalog therefore becomes generic workflow.failed and +transient retry guidance. An owned Git reproduction confirms that absence at +an existing commit and a missing Git object both currently produce Error 128; +localized stderr cannot safely distinguish them. Read-only consumer evidence +independently confirms the configured catalog is absent at its trusted commit; +its unmerged documentation proposal and mutable failure card are separate facts. + +trusted-catalog-R01: the adapter MUST confirm catalog absence through a successful +immutable Git tree lookup rooted at the validated commit, before loading it. +A failed object/read/provider operation MUST retain its original failure class. +Working-tree/candidate copies cannot replace this source. Existing catalog +profile/malformed-data handling and missing template/standard failures retain +separate semantics. Only the fixed specs/catalog.json path enters the new +missing-catalog diagnostic; no repository text or raw Git output is copied. + +trusted-catalog-R02: use a frozen typed missing-catalog fact carrying a validated +immutable SHA. Map it to configuration.unsupported and the operand-free recovery +sdd-catalog-missing-trusted-source. Before publishing one owned status card, +recheck issue body/title and the actual source reference, including an existing +linked branch. Missing freshness, a moved source or failed publication stays +blocked and preserves newer status and the original error. No model analysis, +branch/specification publication, catalog initialization or readiness bypass is +permitted by this diagnostic. + +```text +immutable commit -> bounded catalog tree lookup -> missing catalog fact + SHA + -> independent issue/source freshness -> owned blocked card -> reviewed + prerequisite merged/synchronized -> new trusted revision -> normal gate +``` + +Text equivalent: prove absence at the pinned source, publish only fresh closed +facts, and retry through the normal gate after the prerequisite reaches the +trusted source. The domain owns the typed fact; the Git adapter owns object +lookup; the use case maps semantics and freshness; static EN/ES catalogs and the +existing renderer/upsert own presentation. Existing type/architecture, catalog, +localization and workflow checks enforce those boundaries. No new port, setting, +permission, credential, storage schema, repair budget or external provider integration is added. +The existing read-only source-revision port supplies the freshness check. +The existing 16,000-character card limit and whole-card locale fallback remain. + +Representative card: + +> ## Specification preparation is blocked +> +> **Impact:** specs/catalog.json is missing from the trusted source; model +> analysis has not started. +> +> **Action:** Review and merge the prerequisite through the enabled documentation +> workflow. Update the displayed trusted source through the normal managed-branch +> workflow and retry this same issue after the source changes. +> +> **Retained state:** Answers, any linked branch and published specifications are +> retained; this attempt published no specification or branch. +> +> **Technical details:** configuration.unsupported; correlation reference; +> trusted revision aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; originating run. + +Pending/complete analysis, cancellation and successful recovery use the existing +surfaces. An unchanged missing source remains blocked; provider failure retains +its existing recovery. A moved issue/source withholds an obsolete card. A later +reviewed valid source resumes the same issue and keeps historical answers/state. +Normal package adoption delivers the diagnostic; rollback reverts the typed +mapping and copy together without changing repository/catalog state. Source SHA +is technical evidence, not an approval or branch-creation authority. + +Reserve **35 distinct additional cases before implementation**: + +| Area | Minimum | Behaviors | +|---|---:|---| +| Domain fact | 6 | Immutable source, canonical SHA, invalid/missing/non-string provenance without coercion | +| Git adapter | 10 | Confirmed absence, candidate copy, valid regular catalog, malformed catalog, missing support files, unknown/tree/blob objects and non-file catalog | +| Application/recovery | 9 | Fail before model/publication, replay, source/issue races, retained branch, Spanish/foreign marker, unavailable freshness, genuine provider failure, reviewed-source recovery, publication failure | +| Presentation/security | 9 | EN/ES cause and SHA, invalid provenance, mutually exclusive diagnostic kinds, withheld untrusted recovery operands | +| **Total** | **34** | No double counting with existing SDD cases | + +Keep unchanged global/module floors, 100% new pure-fact coverage, and fully hit +added statements/branches. Acceptance requires RED absent-catalog diagnostics, +all cases, full hooks, types/lint, build/package smoke, documentation, +specification/catalog generation and validation, Graphify, exact-head CI/Codecov, +three native final-cleanup footers and complete review without active findings +before merge. Update docs/issues/pre-branch-sdds.mdx, error-reference recovery, +this owner and the catalog evidence/index together. No material decision remains +open. The immutable lookup follows the official +[git-ls-tree contract](https://git-scm.com/docs/git-ls-tree). + +The budget includes four additional provenance cases before implementation: +terminal line breaks, exact-length non-hex text and non-string renderer input. +A hexadecimal revision must have exactly 40 characters, including at end of input. + +Before extending the existing closed-recovery registry matrix, reserve one +additional case (35 total). Its completeness assertion intentionally rejects a +new recovery ID without matching English/Spanish retained-state coverage. + +Executed local evidence: the 35 reserved cases pass within seven suites containing +395 passing tests and one pre-existing platform-only skip. The new domain fact +and both message catalogs/status renderer have 100% statements, branches, +functions and lines. The focused changed-module group passes 94.57% lines and +92.01% branches; these are focused observations, not the full-repository coverage +certificate. No added runtime statement or branch is uncovered. The independent +owned Git probe now emits the typed missing-catalog fact with its actual commit, +while an unknown Git object remains Error 128. Actual EN/ES renderer previews +include that source SHA and no raw Git command. No consumer code or live issue +was executed or published by these probes. Keep full hooks and fresh remote +merge gates separate and mandatory. diff --git a/src/application/policies/__tests__/application_error_presentation_policy.test.ts b/src/application/policies/__tests__/application_error_presentation_policy.test.ts index ac410251d..f364d4832 100644 --- a/src/application/policies/__tests__/application_error_presentation_policy.test.ts +++ b/src/application/policies/__tests__/application_error_presentation_policy.test.ts @@ -104,6 +104,11 @@ const RECOVERY_CASES: readonly Readonly<{ english: 'The source catalog, answers and published SDDs remain unchanged; no automatic catalog conversion was attempted.', spanish: 'El catálogo de origen, las respuestas y los SDD publicados permanecen intactos; no se intentó convertir el catálogo automáticamente.', }, + { + recovery: { id: 'sdd-catalog-missing-trusted-source', variables: {} }, + english: 'Existing answers, published specifications and any linked branch are retained; this attempt published no specification or branch.', + spanish: 'Se conservan las respuestas, las especificaciones publicadas y cualquier rama enlazada; este intento no publicó ninguna especificación ni rama.', + }, { recovery: { id: 'sdd-draft-format-invalid', variables: {} }, english: 'Existing answers and published SDDs were preserved; no draft was published.', diff --git a/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts b/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts index cd273043b..8ec9c7ea6 100644 --- a/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts +++ b/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts @@ -189,3 +189,35 @@ describe('closed catalog compatibility presentation (#716)', () => { expect(body.length).toBeLessThan(16000); expect(body).toContain('unsupported-profile'); expect(body).toContain('fell back to English'); }); }); + +describe('missing trusted catalog presentation (#808)', () => { + const missing = () => new ApplicationError('configuration.unsupported', 'PRIVATE', { + recovery: { id: 'sdd-catalog-missing-trusted-source', variables: {} }, + }); + it.each(['en-US', 'es-MX'])('shows the bounded cause, trusted SHA and recovery for %s', locale => { + const body = renderSddPreparationFailure({ issueNumber: 42, locale, error: missing(), missingCatalogSource: 'a'.repeat(40), run }); + expect(body).toContain('a'.repeat(40)); expect(body).toContain('specs/catalog.json'); + expect(body).toContain(locale === 'en-US' ? 'Trusted source revision' : 'Revisión de origen confiable'); + expect(body).not.toMatch(/PRIVATE|Specific table facts are unavailable|Catalog compatibility/u); expect(body.length).toBeLessThan(16000); + }); + it.each(['PRIVATE?token=secret', 'a'.repeat(40) + '\n', 'g'.repeat(40)])('rejects unverified source text %p rather than formatting it', source => { + expect(() => renderSddPreparationFailure({ issueNumber: 42, error: missing(), missingCatalogSource: source })).toThrow(TypeError); + }); + it('rejects source objects without coercing their text', () => { + const toString = jest.fn(() => 'a'.repeat(40)); + expect(() => renderSddPreparationFailure({ issueNumber: 42, error: missing(), missingCatalogSource: { toString } as never })).toThrow(TypeError); + expect(toString).not.toHaveBeenCalled(); + }); + it('rejects simultaneous missing and incompatible catalog facts', () => { + expect(() => renderSddPreparationFailure({ issueNumber: 42, error: missing(), missingCatalogSource: 'a'.repeat(40), catalogReason: 'unsupported-profile' })).toThrow(TypeError); + }); + it('rejects simultaneous missing-catalog and draft-table facts', () => { + expect(() => renderSddPreparationFailure({ issueNumber: 42, error: missing(), missingCatalogSource: 'a'.repeat(40), diagnostic: diagnostic() })).toThrow(TypeError); + }); + it('withholds untrusted recovery operands and renders only the closed source fact', () => { + const error = { ...missing(), recovery: { id: 'sdd-catalog-missing-trusted-source', variables: { credential: 'PRIVATE' } } } as never; + const body = renderSddPreparationFailure({ issueNumber: 42, error, missingCatalogSource: 'a'.repeat(40) }); + expect(body).not.toContain('PRIVATE'); + expect(body).toContain('a'.repeat(40)); + }); +}); diff --git a/src/application/policies/application_error_message_catalog.ts b/src/application/policies/application_error_message_catalog.ts index 3b9dbeb2c..97bcb4042 100644 --- a/src/application/policies/application_error_message_catalog.ts +++ b/src/application/policies/application_error_message_catalog.ts @@ -294,6 +294,11 @@ const ENGLISH_RECOVERY_CONTENT: RecoveryContent = Object.freeze({ action: 'Review catalog profile support in the SDD guide. Preserve the native schema, readiness, requirements, evidence and blockers; have a maintainer provide a reviewed compatible adapter, then retry this same issue.', retainedState: 'The repository catalog, answers and published SDDs remain unchanged; no specification or branch was published by this attempt.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'SDD preparation stopped before model analysis because specs/catalog.json is missing from the trusted source revision.', + action: 'Review and merge the catalog prerequisite through the enabled documentation workflow. Update the displayed trusted source through the normal managed-branch workflow, then retry this same issue after the source changes. An unchanged revision remains blocked.', + retainedState: 'Existing answers, published specifications and any linked branch are retained; this attempt published no specification or branch.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'SDD preparation stopped before ownership operations because the catalog cannot establish one valid supported profile and unique owner paths.', action: 'Validate the catalog JSON, supported profile, required metadata and normalized unique owner-path arrays. Preserve repository-owned readiness rules and retry the same issue after maintainer review.', @@ -454,6 +459,11 @@ const SPANISH_RECOVERY_CONTENT: RecoveryContent = Object.freeze({ action: 'Revisa los perfiles admitidos en la guía de SDD. Conserva el esquema nativo, los estados, requisitos, evidencias y bloqueos; pide a un mantenedor un adaptador compatible revisado y reintenta esta misma incidencia.', retainedState: 'El catálogo, las respuestas y los SDD publicados permanecen intactos; este intento no publicó ninguna especificación ni rama.', }), + 'sdd-catalog-missing-trusted-source': Object.freeze({ + impact: 'La preparación del SDD se detuvo antes del análisis del modelo porque falta specs/catalog.json en la revisión de origen confiable.', + action: 'Revisa e integra el catálogo requerido mediante el workflow de documentación habilitado. Actualiza el origen confiable mostrado mediante el flujo normal de ramas gestionadas y reintenta esta misma incidencia cuando cambie esa revisión. La revisión sin cambios permanece bloqueada.', + retainedState: 'Se conservan las respuestas, las especificaciones publicadas y cualquier rama enlazada; este intento no publicó ninguna especificación ni rama.', + }), 'sdd-catalog-format-invalid': Object.freeze({ impact: 'La preparación del SDD se detuvo antes de las operaciones de propiedad porque el catálogo no permite establecer un único perfil admitido con rutas de propiedad válidas y únicas.', action: 'Valida el JSON del catálogo, el perfil admitido, los metadatos obligatorios y las listas de rutas de propiedad normalizadas y únicas. Conserva las reglas de preparación del repositorio y reintenta la misma incidencia tras la revisión de un mantenedor.', diff --git a/src/application/policies/sdd_preparation_message_catalog.ts b/src/application/policies/sdd_preparation_message_catalog.ts index dc44943dd..3ff936e43 100644 --- a/src/application/policies/sdd_preparation_message_catalog.ts +++ b/src/application/policies/sdd_preparation_message_catalog.ts @@ -5,6 +5,8 @@ import type { MessageCatalogResolutionPort } from '../ports/message_catalog_port import { resolveMessageCatalogView, resolveStaticMessageCatalogView } from './resolved_message_catalog_policy'; const UI = Object.freeze({ + missingCatalogSource: ['Trusted source revision', 'Revisión de origen confiable'] as const, + missingCatalogFact: ['Missing prerequisite: specs/catalog.json.', 'Requisito ausente: specs/catalog.json.'] as const, catalogCompatibility: ['Catalog compatibility', 'Compatibilidad del catálogo'] as const, catalogProfile: ['The installed automated writer supports the SDK version-1 profile with specs arrays. A repository-selected schema requires a reviewed compatible adapter.', 'El publicador automático instalado admite el perfil SDK de versión 1 con listas specs. Un esquema seleccionado por el repositorio requiere un adaptador compatible revisado.'] as const, catalogRetained: ['Native schema, readiness, requirements, design and implementation evidence, and blockers remain unchanged. Review the supported-profile boundary before retrying this same issue.', 'El esquema nativo, los estados, requisitos, evidencias de diseño e implementación y bloqueos permanecen intactos. Revisa los perfiles admitidos antes de reintentar esta misma incidencia.'] as const, diff --git a/src/application/policies/sdd_preparation_status_policy.ts b/src/application/policies/sdd_preparation_status_policy.ts index 9f8163d0c..4a654b725 100644 --- a/src/application/policies/sdd_preparation_status_policy.ts +++ b/src/application/policies/sdd_preparation_status_policy.ts @@ -33,6 +33,7 @@ export function renderSddPreparationFailure(options: Readonly<{ error: ApplicationErrorPresentationSource; diagnostic?: SddValidationDiagnostic; catalogReason?: SddCatalogCompatibilityReason; + missingCatalogSource?: string; attempts?: 1 | 2; publicationPending?: boolean; run?: SddWorkflowRunFacts; @@ -42,6 +43,11 @@ export function renderSddPreparationFailure(options: Readonly<{ || typeof options.error.retryable !== 'boolean' || !isApplicationErrorCorrelationId(options.error.correlationId)) { throw new TypeError('Invalid SDD status identity or semantic error facts.'); } + const missingCatalogSource = options.missingCatalogSource; + if (missingCatalogSource !== undefined && (typeof missingCatalogSource !== 'string' + || !/^[a-f0-9]{40}$/u.test(missingCatalogSource) || options.catalogReason !== undefined || options.diagnostic !== undefined)) { + throw new TypeError('Invalid exclusive missing trusted-catalog facts.'); + } if (options.catalogReason !== undefined && (!SDD_CATALOG_COMPATIBILITY_REASONS.includes(options.catalogReason) || options.diagnostic)) { throw new TypeError('Invalid exclusive catalog compatibility facts.'); } @@ -66,7 +72,10 @@ export function renderSddPreparationFailure(options: Readonly<{ `**${message('sddPreparation.code')}:** \`${view.code}\``, `**${message('sddPreparation.reference')}:** \`${view.reference}\``, ]; - if (options.catalogReason) { + if (missingCatalogSource) { + lines.push(`**${message('sddPreparation.missingCatalogSource')}:** \`${missingCatalogSource}\``, + '', message('sddPreparation.missingCatalogFact')); + } else if (options.catalogReason) { lines.push(`**${message('sddPreparation.catalogCompatibility')}:** \`${options.catalogReason}\``, '', message('sddPreparation.catalogProfile'), '', message('sddPreparation.catalogRetained')); } else if (diagnostic) { diff --git a/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts b/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts index 6be2f6e5b..b64b3e70b 100644 --- a/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts +++ b/src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts @@ -1,4 +1,5 @@ import { allowsEmptySddCatalogPathField, SddCatalogCompatibilityFailure } from '../../../domain/sdd_catalog_compatibility'; +import { SddTrustedCatalogMissingFailure } from '../../../domain/sdd_trusted_catalog_failure'; import { SddDraftPreparationFailure } from '../../errors/sdd_draft_preparation_failure'; import { renderSddPreparationFailure, renderSddPreparationRecovered, type SddWorkflowRunFacts } from '../../policies/sdd_preparation_status_policy'; import { resolveSddPreparationCatalog, resolveStaticSddPreparationCatalog } from '../../policies/sdd_preparation_message_catalog'; @@ -242,7 +243,7 @@ export class PreBranchSddGateUseCase { await this.assertFresh(context, snapshot.baseSha, sourceBranch); return { status: 'drafted', prepared, record, ...(card ? { cardId: card.id } : {}), results: [this.result(true, true, `Validated ${plan.path} before branch publication.`)] }; } catch (error) { - if (error instanceof SddCatalogCompatibilityFailure && error.sourceCommit) { + if ((error instanceof SddCatalogCompatibilityFailure || error instanceof SddTrustedCatalogMissingFailure) && error.sourceCommit) { failureSource = { branch: failureBranch, sha: error.sourceCommit }; } const failure = this.failure(error); @@ -346,7 +347,7 @@ export class PreBranchSddGateUseCase { private async writePreparationFailure(context: PreBranchSddContext, error: ApplicationError, cause: unknown, publicationPending: boolean, source?: Readonly<{ branch: string; sha: string }>): Promise { if (!context.tokenUser.trim()) throw new ApplicationError('configuration.unsupported', 'The SDD diagnostic card requires a trusted bot identity.'); - if (cause instanceof SddCatalogCompatibilityFailure && !source) throw new ApplicationError('configuration.unsupported', 'The catalog compatibility diagnostic requires a verified source revision.'); + if ((cause instanceof SddCatalogCompatibilityFailure || cause instanceof SddTrustedCatalogMissingFailure) && !source) throw new ApplicationError('configuration.unsupported', 'The catalog diagnostic requires a verified source revision.'); const comments = await this.comments.listIssueComments(context.issueNumber); const [catalog, errorCatalog] = await Promise.all([ resolveSddPreparationCatalog(context.issueLocale ?? 'en-US', context.agentConfiguration, this.presentation.catalogResolver), @@ -357,6 +358,7 @@ export class PreBranchSddGateUseCase { error, publicationPending, run: this.presentation.run, ...(cause instanceof SddDraftPreparationFailure ? { diagnostic: cause.diagnostic, attempts: cause.attempts } : {}), ...(cause instanceof SddCatalogCompatibilityFailure ? { catalogReason: cause.reason } : {}), + ...(cause instanceof SddTrustedCatalogMissingFailure ? { missingCatalogSource: cause.sourceCommit } : {}), }, aligned ? catalog : resolveStaticSddPreparationCatalog('en-US'), aligned ? errorCatalog.message : resolveStaticApplicationErrorCatalog('en-US').message) + (aligned ? '' : '\n\n' + resolveStaticSddPreparationCatalog('en-US').message('sddPreparation.fallbackSlices')); @@ -412,7 +414,10 @@ export class PreBranchSddGateUseCase { private failure(error: unknown): Result { // The runtime logger redacts credentials and makes workflow-command text inert. logError(`Pre-branch SDD gate failed: ${error instanceof Error ? error.message : 'unknown failure'}`); - const semanticError = error instanceof SddCatalogCompatibilityFailure + const semanticError = error instanceof SddTrustedCatalogMissingFailure + ? new ApplicationError('configuration.unsupported', error.message, { recovery: { + id: 'sdd-catalog-missing-trusted-source', variables: {}, + } }) : error instanceof SddCatalogCompatibilityFailure ? new ApplicationError('configuration.unsupported', error.message, { recovery: { id: error.reason === 'unsupported-profile' ? 'sdd-catalog-unsupported-profile' : 'sdd-catalog-format-invalid', variables: {}, } }) : toApplicationError(error, 'workflow.failed', 'The pre-branch SDD gate is blocked.'); diff --git a/src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts b/src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts index 76ba0b9f7..0d1f860ca 100644 --- a/src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts +++ b/src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts @@ -1,4 +1,5 @@ import { SddCatalogCompatibilityFailure, SDD_CATALOG_COMPATIBILITY_REASONS } from '../../../../../domain/sdd_catalog_compatibility'; +import { SddTrustedCatalogMissingFailure } from '../../../../../domain/sdd_trusted_catalog_failure'; import { readSddGateRecord, SddDraftFormatFailure } from '../../../../../domain/pre_branch_sdd'; import { PreBranchSddGateUseCase, type PreBranchSddContext } from '../../../sdd/pre_branch_sdd_gate_use_case'; import type { SddCatalogSnapshot, SddPreparedDraft } from '../../../../ports/pre_branch_sdd_ports'; @@ -1178,3 +1179,78 @@ describe('catalog profile preflight and recovery (#716)', () => { expect(h.publish).toHaveBeenCalledTimes(1); }); }); + +describe('confirmed missing trusted catalog recovery (#808)', () => { + const missing = () => new SddTrustedCatalogMissingFailure(baseSha); + it('blocks before analysis/publication and reuses one fresh prerequisite card on replay', async () => { + const h = harness(undefined, true); h.readSourceRevision.mockResolvedValue(baseSha); h.loadSnapshot.mockRejectedValue(missing()); + for (let replay = 0; replay < 2; replay++) { + const outcome = await h.useCase.begin(context()); expect(outcome.status).toBe('blocked'); + expect(outcome.results[0].errors[0]).toMatchObject({ code: 'configuration.unsupported', recovery: { + id: 'sdd-catalog-missing-trusted-source', variables: {}, + } }); + } + expect(h.comments).toHaveLength(1); expect(h.comments[0].body).toContain(baseSha); + expect(h.comments[0].body).toContain('specs/catalog.json'); expect(h.comments[0].body).toContain('after the source changes'); + expect(h.comments[0].body).not.toContain('Catalog compatibility'); expect(h.query).not.toHaveBeenCalled(); + expect(h.validateDraft).not.toHaveBeenCalled(); expect(h.publish).not.toHaveBeenCalled(); expect(h.labels()).not.toContain('branched'); + }); + it.each(['base', 'issue'] as const)('withholds a missing-catalog card after a %s race', async race => { + const h = harness(undefined, true); h.loadSnapshot.mockRejectedValue(missing()); + h.readSourceRevision.mockResolvedValue(race === 'base' ? commitSha : baseSha); + if (race === 'issue') h.getDescription.mockResolvedValue('New human request'); + const current = ' Current status'; + h.comments.push({ id: 900, body: current, user: { login: 'copilot[bot]' } }); + const outcome = await h.useCase.begin(context()); + expect(outcome.results.flatMap(result => result.errors)).toEqual(expect.arrayContaining([expect.objectContaining({ code: 'workflow.stale' })])); + expect(h.comments[0].body).toBe(current); expect(h.updateComment).not.toHaveBeenCalled(); expect(h.query).not.toHaveBeenCalled(); + }); + it('checks a retained linked branch and preserves its published record', async () => { + const h = harness(undefined, true); h.readSourceRevision.mockResolvedValue(baseSha); + h.query.mockResolvedValueOnce({ ...plan, newCapability: null }).mockResolvedValue({ markdown: '# Draft' }); + const draft = await h.useCase.begin(context()); if (draft.status !== 'drafted') throw new Error('Expected draft'); + await h.useCase.publish(context(), draft, 'feature/42-change'); + const record = h.comments.find(comment => readSddGateRecord(comment.body, 42))!.body; + h.loadSnapshot.mockRejectedValue(new SddTrustedCatalogMissingFailure(commitSha)); h.readSourceRevision.mockResolvedValue(commitSha); + h.readSourceRevision.mockClear(); h.query.mockClear(); + expect((await h.useCase.begin(context())).status).toBe('blocked'); + expect(h.readSourceRevision).toHaveBeenCalledWith('feature/42-change'); expect(h.query).not.toHaveBeenCalled(); + expect(h.comments.find(comment => readSddGateRecord(comment.body, 42))!.body).toBe(record); + expect(h.publish).toHaveBeenCalledTimes(1); + }); + it('localizes missing prerequisites in Spanish without modifying a foreign marker', async () => { + const h = harness(undefined, true); h.readSourceRevision.mockResolvedValue(baseSha); h.loadSnapshot.mockRejectedValue(missing()); + const human = ' PRIVATE_HUMAN'; + h.comments.push({ id: 90, body: human, user: { login: 'alice' } }); + expect((await h.useCase.begin({ ...context(), issueLocale: 'es-MX' })).status).toBe('blocked'); + expect(h.comments[0].body).toBe(human); expect(h.comments[1].body).toContain('falta specs/catalog.json'); + expect(h.comments[1].body).toContain(baseSha); expect(h.comments[1].body).not.toContain('PRIVATE_HUMAN'); + }); + it('fails closed when legacy freshness would have to reload the missing catalog', async () => { + const h = harness(); h.loadSnapshot.mockRejectedValue(missing()); + const outcome = await h.useCase.begin(context()); expect(outcome.status).toBe('blocked'); expect(outcome.results).toHaveLength(2); + expect(h.comments).toHaveLength(0); expect(h.query).not.toHaveBeenCalled(); expect(h.publish).not.toHaveBeenCalled(); + }); + it('keeps unrelated read failures on their existing workflow failure path', async () => { + const h = harness(undefined, true); h.loadSnapshot.mockRejectedValue(new Error('PRIVATE_PROVIDER_FAILURE')); + const outcome = await h.useCase.begin(context()); expect(outcome.status).toBe('blocked'); + expect(outcome.results[0].errors[0].code).toBe('workflow.failed'); + expect(h.comments[0].body).not.toMatch(/PRIVATE_PROVIDER_FAILURE|Trusted source revision/u); expect(h.query).not.toHaveBeenCalled(); + }); + it('resumes normal drafting only after a new reviewed source becomes available', async () => { + const h = harness(undefined, true); h.readSourceRevision.mockResolvedValue(baseSha); h.loadSnapshot.mockRejectedValueOnce(missing()); + expect((await h.useCase.begin(context())).status).toBe('blocked'); expect(h.query).not.toHaveBeenCalled(); + h.loadSnapshot.mockResolvedValue({ ...snapshot, baseSha: commitSha }); h.readSourceRevision.mockResolvedValue(commitSha); + h.validateDraft.mockResolvedValue({ ...prepared, baseSha: commitSha }); + h.query.mockResolvedValueOnce({ ...plan, newCapability: null }).mockResolvedValue({ markdown: '# Draft' }); + expect((await h.useCase.begin(context())).status).toBe('drafted'); expect(h.validateDraft).toHaveBeenCalledTimes(1); + expect(h.comments.filter(comment => comment.body.includes('copilot:sdd-analysis-status:v1'))).toHaveLength(1); + }); + it('retains the missing-prerequisite error when its status publication fails', async () => { + const h = harness(undefined, true); h.readSourceRevision.mockResolvedValue(baseSha); h.loadSnapshot.mockRejectedValue(missing()); + h.addComment.mockRejectedValue(new Error('PRIVATE_WRITE_FAILURE')); + const outcome = await h.useCase.begin(context()); expect(outcome.status).toBe('blocked'); + expect(outcome.results[0].errors[0].recovery?.id).toBe('sdd-catalog-missing-trusted-source'); + expect(outcome.results).toHaveLength(2); expect(h.publish).not.toHaveBeenCalled(); expect(h.query).not.toHaveBeenCalled(); + }); +}); diff --git a/src/data/model/application_error.ts b/src/data/model/application_error.ts index 8b9755663..cb9c383b3 100644 --- a/src/data/model/application_error.ts +++ b/src/data/model/application_error.ts @@ -62,6 +62,7 @@ export const APPLICATION_ERROR_RECOVERY_IDS = Object.freeze([ 'sdd-draft-size-exceeded', 'sdd-catalog-unsupported-profile', 'sdd-catalog-format-invalid', + 'sdd-catalog-missing-trusted-source', 'sdd-draft-format-invalid', 'sdd-draft-case-identity-invalid', 'sdd-draft-case-budget-invalid', @@ -110,6 +111,7 @@ interface ApplicationErrorRecoveryVariables extends Readonly>; readonly 'sdd-catalog-unsupported-profile': Readonly>; readonly 'sdd-catalog-format-invalid': Readonly>; + readonly 'sdd-catalog-missing-trusted-source': Readonly>; readonly 'sdd-draft-format-invalid': Readonly>; readonly 'sdd-draft-case-identity-invalid': Readonly>; readonly 'sdd-draft-case-budget-invalid': Readonly>; @@ -399,6 +401,7 @@ const RECOVERY_VARIABLE_KEYS: Readonly { + it('retains only immutable provenance and fixed prerequisite copy', () => { + const error = new SddTrustedCatalogMissingFailure('a'.repeat(40)); + expect(error.sourceCommit).toBe('a'.repeat(40)); + expect(error.message).toBe('The trusted source does not contain specs/catalog.json.'); + expect(Object.isFrozen(error)).toBe(true); + }); + it('normalizes the same hexadecimal source identity', () => { + expect(new SddTrustedCatalogMissingFailure('AB'.repeat(20)).sourceCommit).toBe('ab'.repeat(20)); + }); + it.each(['a'.repeat(40) + '\n', 'g' + 'a'.repeat(39)])('rejects malformed provenance %p', source => { + expect(() => new SddTrustedCatalogMissingFailure(source)).toThrow(TypeError); + }); + it('rejects absent provenance', () => { + expect(() => new SddTrustedCatalogMissingFailure(undefined as never)).toThrow(TypeError); + }); + it('does not coerce an object into trusted provenance', () => { + const toString = jest.fn(() => 'a'.repeat(40)); + expect(() => new SddTrustedCatalogMissingFailure({ toString } as never)).toThrow(TypeError); + expect(toString).not.toHaveBeenCalled(); + }); +}); diff --git a/src/domain/sdd_trusted_catalog_failure.ts b/src/domain/sdd_trusted_catalog_failure.ts new file mode 100644 index 000000000..36bb5cc8f --- /dev/null +++ b/src/domain/sdd_trusted_catalog_failure.ts @@ -0,0 +1,14 @@ +/** Closed prerequisite fact; never carries provider stderr or candidate content. */ +export class SddTrustedCatalogMissingFailure extends Error { + readonly sourceCommit: string; + + constructor(sourceCommit: string) { + if (typeof sourceCommit !== 'string' || !/^[a-f0-9]{40}$/iu.test(sourceCommit)) { + throw new TypeError('Invalid trusted SDD catalog source revision.'); + } + super('The trusted source does not contain specs/catalog.json.'); + this.name = 'SddTrustedCatalogMissingFailure'; + this.sourceCommit = sourceCommit.toLowerCase(); + Object.freeze(this); + } +} diff --git a/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts b/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts index 3e990ae33..037e63c83 100644 --- a/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts +++ b/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts @@ -1,4 +1,5 @@ import fs from 'node:fs'; +import { SddTrustedCatalogMissingFailure } from '../../domain/sdd_trusted_catalog_failure'; import { sddConsistencyFixture } from '../../testing/sdd_consistency_fixture'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -507,3 +508,56 @@ describe('PreBranchSddWorkspaceAdapter with a local bare remote', () => { expect(fs.readFileSync(external, 'utf8')).toBe('# Protected external file\n'); }); }); + +describe('missing catalog at a trusted immutable commit (#808)', () => { + let temp: string; + let repo: string; + beforeEach(() => { ({ temp, repo } = fixture()); }); + afterEach(() => fs.rmSync(temp, { recursive: true, force: true })); + const commit = () => { + git(repo, 'add', '-A'); git(repo, 'commit', '-m', 'Owned prerequisite change'); + return git(repo, 'rev-parse', 'HEAD'); + }; + it('proves catalog absence at an existing source commit', async () => { + fs.unlinkSync(path.join(repo, 'specs/catalog.json')); const sha = commit(); + await expect(new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(sha)) + .rejects.toMatchObject({ name: 'SddTrustedCatalogMissingFailure', sourceCommit: sha }); + }); + it('ignores an unreviewed candidate copy and recovers only at its new commit', async () => { + const file = path.join(repo, 'specs/catalog.json'); const catalog = fs.readFileSync(file, 'utf8'); + fs.unlinkSync(file); const missing = commit(); fs.writeFileSync(file, catalog); + const workspace = new PreBranchSddWorkspaceAdapter(repo); + await expect(workspace.loadSnapshotAtCommit(missing)).rejects.toBeInstanceOf(SddTrustedCatalogMissingFailure); + const reviewed = commit(); expect((await workspace.loadSnapshotAtCommit(reviewed)).baseSha).toBe(reviewed); + await expect(workspace.loadSnapshotAtCommit(missing)).rejects.toBeInstanceOf(SddTrustedCatalogMissingFailure); + }); + it('uses a regular committed catalog despite a malformed working-tree copy', async () => { + const sha = git(repo, 'rev-parse', 'HEAD'); fs.writeFileSync(path.join(repo, 'specs/catalog.json'), '{'); + const loaded = await new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(sha); + expect(loaded.baseSha).toBe(sha); expect(loaded.capabilities[0].id).toBe('payments'); + }); + it('keeps malformed existing catalog precedence over a missing template', async () => { + fs.writeFileSync(path.join(repo, 'specs/catalog.json'), '{'); fs.unlinkSync(path.join(repo, 'specs/_template.md')); + const sha = commit(); + await expect(new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(sha)) + .rejects.toMatchObject({ name: 'SddCatalogCompatibilityFailure', reason: 'invalid-json', sourceCommit: sha }); + }); + it.each(['specs/_template.md', 'specs/README.md'])('does not report a missing catalog for an absent %s', async file => { + fs.unlinkSync(path.join(repo, file)); const sha = commit(); + const error = await new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(sha).catch(error => error); + expect(error).toMatchObject({ message: expect.any(String), code: 128 }); expect(error).not.toBeInstanceOf(SddTrustedCatalogMissingFailure); + expect(error.code).toBe(128); + }); + it.each(['unknown', 'tree', 'blob'] as const)('does not misclassify the %s Git object as confirmed catalog absence', async kind => { + const sha = kind === 'unknown' ? '0'.repeat(40) : git(repo, 'rev-parse', kind === 'tree' ? 'HEAD^{tree}' : 'HEAD:specs/catalog.json'); + const error = await new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(sha).catch(error => error); + expect(error).toMatchObject({ message: expect.any(String), code: 128 }); expect(error).not.toBeInstanceOf(SddTrustedCatalogMissingFailure); + expect(error.code).toBe(128); + }); + it('keeps a catalog directory distinct from confirmed path absence', async () => { + const file = path.join(repo, 'specs/catalog.json'); fs.unlinkSync(file); fs.mkdirSync(file); + fs.writeFileSync(path.join(file, 'candidate.json'), '{}'); const sha = commit(); + const error = await new PreBranchSddWorkspaceAdapter(repo).loadSnapshotAtCommit(sha).catch(error => error); + expect(error).toMatchObject({ name: 'SddCatalogCompatibilityFailure', reason: 'invalid-json', sourceCommit: sha }); + }); +}); diff --git a/src/infrastructure/pre_branch_sdd_workspace_adapter.ts b/src/infrastructure/pre_branch_sdd_workspace_adapter.ts index 08e244b28..80c16e012 100644 --- a/src/infrastructure/pre_branch_sdd_workspace_adapter.ts +++ b/src/infrastructure/pre_branch_sdd_workspace_adapter.ts @@ -1,4 +1,5 @@ import { readSupportedSddCatalog, SddCatalogCompatibilityFailure } from '../domain/sdd_catalog_compatibility'; +import { SddTrustedCatalogMissingFailure } from '../domain/sdd_trusted_catalog_failure'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -50,6 +51,9 @@ export class PreBranchSddWorkspaceAdapter implements PreBranchSddWorkspacePort { } private async readSnapshotAtSha(baseSha: string): Promise { + // A successful immutable lookup proves absence; object/read failures remain Git failures. + const catalogEntry = await this.git(['ls-tree', '-z', '--full-tree', `${baseSha}^{commit}`, '--', 'specs/catalog.json']); + if (catalogEntry === '') throw new SddTrustedCatalogMissingFailure(baseSha); const raw = await this.git(['show', `${baseSha}:specs/catalog.json`]); let catalog; try { catalog = readSupportedSddCatalog(raw); }