diff --git a/.github/workflows/api-diff-check.yml b/.github/workflows/api-diff-check.yml index 15824119a..138d37416 100644 --- a/.github/workflows/api-diff-check.yml +++ b/.github/workflows/api-diff-check.yml @@ -35,6 +35,18 @@ jobs: cache: pnpm - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Verify OpenAPI spec is up to date + run: |- + set -euo pipefail + # Regenerate the spec + types from the Zod schemas and fail if the committed copy is stale + # (e.g. a schema change was committed with --no-verify, bypassing the pre-commit hook). + pnpm --filter @easy-genomics/shared-lib run generate:openapi + pnpm --filter @easy-genomics/shared-lib run generate:api-types + if ! git diff --quiet -- packages/shared-lib/src/app/openapi/easy-genomics-api.yaml packages/shared-lib/src/app/types/easy-genomics/generated.d.ts; then + echo "::error::OpenAPI spec/types are out of date. Run 'pnpm --filter @easy-genomics/shared-lib run generate:openapi && pnpm --filter @easy-genomics/shared-lib run generate:api-types' and commit the result." + git --no-pager diff -- packages/shared-lib/src/app/openapi/easy-genomics-api.yaml packages/shared-lib/src/app/types/easy-genomics/generated.d.ts + exit 1 + fi - name: Run optic diff id: diff run: |- diff --git a/.husky/pre-commit b/.husky/pre-commit index 8b58f4149..7dfc0c9a5 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,4 +3,7 @@ pnpm exec validate-branch-name -r '^(main|release){1}$|^(feat|fix|hotfix|infra|release|refactor|chore|docs)/.+$' pnpm pre-commit pnpm exec projen +pnpm --filter @easy-genomics/shared-lib run generate:openapi +pnpm --filter @easy-genomics/shared-lib run generate:api-types +git add packages/shared-lib/src/app/openapi/easy-genomics-api.yaml packages/shared-lib/src/app/types/easy-genomics/generated.d.ts pnpm --filter @easy-genomics/back-end test -- --silent diff --git a/packages/shared-lib/src/app/openapi/easy-genomics-api.yaml b/packages/shared-lib/src/app/openapi/easy-genomics-api.yaml index eea57ef40..8f09f67cc 100644 --- a/packages/shared-lib/src/app/openapi/easy-genomics-api.yaml +++ b/packages/shared-lib/src/app/openapi/easy-genomics-api.yaml @@ -491,6 +491,7 @@ components: - input_bam - input_cram - input_vcf + - assembly_fasta - metadata - custom_uri required: @@ -517,6 +518,7 @@ components: additionalProperties: false SampleSheetColumnRole: enum: + - assembly_fasta - custom_uri - input_bam - input_cram @@ -729,6 +731,7 @@ components: - input_bam - input_cram - input_vcf + - assembly_fasta - metadata - custom_uri required: @@ -1359,6 +1362,7 @@ components: - input_bam - input_cram - input_vcf + - assembly_fasta - metadata - custom_uri required: @@ -1518,6 +1522,28 @@ components: minimum: 0 EnableNewWorkflowsByDefault: type: boolean + HealthOmicsLlmProvider: + type: string + enum: + - bedrock + - openai + - anthropic + HealthOmicsLlmModelId: + type: string + SeqeraLlmProvider: + type: string + enum: + - bedrock + - openai + - anthropic + SeqeraLlmModelId: + type: string + HealthOmicsLogEnrichmentEnabled: + type: boolean + HealthOmicsLlmApiKey: + type: string + SeqeraLlmApiKey: + type: string required: - OrganizationId - Name @@ -1565,6 +1591,45 @@ components: Laboratory-wide run retention policy, in months, applied after a run reaches a terminal state. - 0 means "never delete run records" (no TTL expiration). type: number + HealthOmicsLlmProvider: + description: |- + BYOK LLM provider selection per integration. Each lab can pick a different + provider/model/key for HealthOmics vs Seqera. Setting a provider IS the + enable signal — when set, ambiguous HealthOmics failures and free-text + Seqera errors are routed to the configured LLM. The deterministic + HealthOmics lookup table runs regardless. Bedrock uses the platform Lambda + IAM; OpenAI / Anthropic read the lab's own API key from SSM at classify + time. + + SSM paths for the API keys: + `/easy-genomics/organization/{OrganizationId}/laboratory/{LaboratoryId}/llm-api-key-healthomics` + `/easy-genomics/organization/{OrganizationId}/laboratory/{LaboratoryId}/llm-api-key-seqera` + enum: + - anthropic + - bedrock + - openai + type: string + HealthOmicsLlmModelId: + type: string + SeqeraLlmProvider: + enum: + - anthropic + - bedrock + - openai + type: string + SeqeraLlmModelId: + type: string + HealthOmicsLogEnrichmentEnabled: + description: |- + When true, the failure classifier fetches the failed HealthOmics run's + CloudWatch engine log, redacts PII + secrets, and sends a bounded excerpt to + the configured LLM for deeper diagnosis. Requires a HealthOmics LLM provider. + type: boolean + HasHealthOmicsLlmApiKey: + description: Boolean indicators returned by read-laboratory; the actual keys never leave SSM. + type: boolean + HasSeqeraLlmApiKey: + type: boolean CreatedAt: type: string CreatedBy: @@ -1616,10 +1681,33 @@ components: type: number EnableNewWorkflowsByDefault: type: boolean + HealthOmicsLlmProvider: + enum: + - anthropic + - bedrock + - openai + type: string + HealthOmicsLlmModelId: + type: string + SeqeraLlmProvider: + enum: + - anthropic + - bedrock + - openai + type: string + SeqeraLlmModelId: + type: string + HealthOmicsLogEnrichmentEnabled: + type: boolean HasNextFlowTowerAccessToken: type: boolean HasGitHubAccessToken: type: boolean + HasHealthOmicsLlmApiKey: + description: Boolean indicators. The actual keys live in SSM and are never returned. + type: boolean + HasSeqeraLlmApiKey: + type: boolean required: - LaboratoryId - Name @@ -1768,6 +1856,49 @@ components: Populated by the status-check processor from Seqera `workflow.duration` or AWS HealthOmics `stopTime - startTime`. type: number + FailureReason: + description: |- + Top-level failure reason reported by the platform when a run reaches FAILED state. + Sourced from HealthOmics `failureReason` or Seqera `workflow.errorMessage`. + Absent on runs that failed before this field was introduced. + type: string + FailureStatusMessage: + description: |- + HealthOmics human-readable `statusMessage` (often carries the failing task name + and a CloudWatch log link). Kept separate from the machine-code `FailureReason` + so the classifier receives both signals. + type: string + FailureErrorReport: + description: |- + Seqera `workflow.errorReport` — the richer Nextflow stack-trace / error detail, + distinct from the one-line `workflow.errorMessage` stored in `FailureReason`. + type: string + FailureOwner: + description: |- + Party responsible for resolving a failure: Lab user (input/data), Bioinformatician + (workflow definition), AWS (transient, retry), or Ambiguous (needs investigation). + Populated asynchronously by the classification pipeline after a FAILED transition. + enum: + - AWS + - Ambiguous + - Bioinformatician + - Lab + type: string + FailureSummary: + description: One-line human summary of the failure suitable for inline display. + type: string + FailureAction: + description: Suggested next step, imperative voice (e.g. "Increase memory allocation"). + type: string + FailureClassifiedBy: + description: |- + Provenance of the classification: `lookup` = deterministic table hit, + `llm` = produced by the configured LLM provider. Used to render an + AI-assisted disclaimer in the UI and for ops debugging. + enum: + - llm + - lookup + type: string required: - LaboratoryId - OrganizationId @@ -1837,6 +1968,28 @@ components: type: number RunDurationSeconds: type: number + FailureReason: + type: string + FailureStatusMessage: + type: string + FailureErrorReport: + type: string + FailureOwner: + enum: + - AWS + - Ambiguous + - Bioinformatician + - Lab + type: string + FailureSummary: + type: string + FailureAction: + type: string + FailureClassifiedBy: + enum: + - llm + - lookup + type: string required: - LaboratoryId - OrganizationId @@ -1896,6 +2049,28 @@ components: minimum: 0 EnableNewWorkflowsByDefault: type: boolean + HealthOmicsLlmProvider: + type: string + enum: + - bedrock + - openai + - anthropic + HealthOmicsLlmModelId: + type: string + SeqeraLlmProvider: + type: string + enum: + - bedrock + - openai + - anthropic + SeqeraLlmModelId: + type: string + HealthOmicsLogEnrichmentEnabled: + type: boolean + HealthOmicsLlmApiKey: + type: string + SeqeraLlmApiKey: + type: string required: - Name - Status @@ -2553,6 +2728,12 @@ components: type: array items: $ref: '#/components/schemas/FavouriteWorkflow' + AnalyticsConsent: + enum: + - denied + - granted + - unset + type: string CreatedAt: type: string CreatedBy: @@ -2613,6 +2794,12 @@ components: - Platform - LaboratoryId additionalProperties: false + AnalyticsConsent: + type: string + enum: + - unset + - granted + - denied additionalProperties: false ListComputeEnvsResponse: type: object diff --git a/packages/shared-lib/src/app/types/easy-genomics/generated.d.ts b/packages/shared-lib/src/app/types/easy-genomics/generated.d.ts index 8ed727879..6c20e1f99 100644 --- a/packages/shared-lib/src/app/types/easy-genomics/generated.d.ts +++ b/packages/shared-lib/src/app/types/easy-genomics/generated.d.ts @@ -587,7 +587,7 @@ export interface components { Columns: ({ columnName: string; /** @enum {string} */ - role: "sample_id" | "read1" | "read2" | "reads" | "reference_fasta" | "reference_gtf" | "reference_gff" | "reference_bed" | "input_bam" | "input_cram" | "input_vcf" | "metadata" | "custom_uri"; + role: "sample_id" | "read1" | "read2" | "reads" | "reference_fasta" | "reference_gtf" | "reference_gff" | "reference_bed" | "input_bam" | "input_cram" | "input_vcf" | "assembly_fasta" | "metadata" | "custom_uri"; required: boolean; })[]; SampleIds?: string[]; @@ -595,7 +595,7 @@ export interface components { ExistingSequenceCollectionId?: string; }; /** @enum {string} */ - SampleSheetColumnRole: "custom_uri" | "input_bam" | "input_cram" | "input_vcf" | "metadata" | "read1" | "read2" | "reads" | "reference_bed" | "reference_fasta" | "reference_gff" | "reference_gtf" | "sample_id"; + SampleSheetColumnRole: "assembly_fasta" | "custom_uri" | "input_bam" | "input_cram" | "input_vcf" | "metadata" | "read1" | "read2" | "reads" | "reference_bed" | "reference_fasta" | "reference_gff" | "reference_gtf" | "sample_id"; LaboratorySequenceCollection: { SequenceCollectionId: string; Name: string; @@ -665,7 +665,7 @@ export interface components { Columns: ({ columnName: string; /** @enum {string} */ - role: "sample_id" | "read1" | "read2" | "reads" | "reference_fasta" | "reference_gtf" | "reference_gff" | "reference_bed" | "input_bam" | "input_cram" | "input_vcf" | "metadata" | "custom_uri"; + role: "sample_id" | "read1" | "read2" | "reads" | "reference_fasta" | "reference_gtf" | "reference_gff" | "reference_bed" | "input_bam" | "input_cram" | "input_vcf" | "assembly_fasta" | "metadata" | "custom_uri"; required: boolean; })[]; SampleIds: string[]; @@ -913,7 +913,7 @@ export interface components { Columns: ({ columnName: string; /** @enum {string} */ - role: "sample_id" | "read1" | "read2" | "reads" | "reference_fasta" | "reference_gtf" | "reference_gff" | "reference_bed" | "input_bam" | "input_cram" | "input_vcf" | "metadata" | "custom_uri"; + role: "sample_id" | "read1" | "read2" | "reads" | "reference_fasta" | "reference_gtf" | "reference_gff" | "reference_bed" | "input_bam" | "input_cram" | "input_vcf" | "assembly_fasta" | "metadata" | "custom_uri"; required: boolean; })[]; }; @@ -975,6 +975,15 @@ export interface components { NextFlowTowerWorkspaceId?: string; RunRetentionMonths?: number; EnableNewWorkflowsByDefault?: boolean; + /** @enum {string} */ + HealthOmicsLlmProvider?: "bedrock" | "openai" | "anthropic"; + HealthOmicsLlmModelId?: string; + /** @enum {string} */ + SeqeraLlmProvider?: "bedrock" | "openai" | "anthropic"; + SeqeraLlmModelId?: string; + HealthOmicsLogEnrichmentEnabled?: boolean; + HealthOmicsLlmApiKey?: string; + SeqeraLlmApiKey?: string; }; /** @enum {string} */ Status: "Active" | "Inactive"; @@ -1001,6 +1010,34 @@ export interface components { * - 0 means "never delete run records" (no TTL expiration). */ RunRetentionMonths?: number; + /** + * @description BYOK LLM provider selection per integration. Each lab can pick a different + * provider/model/key for HealthOmics vs Seqera. Setting a provider IS the + * enable signal — when set, ambiguous HealthOmics failures and free-text + * Seqera errors are routed to the configured LLM. The deterministic + * HealthOmics lookup table runs regardless. Bedrock uses the platform Lambda + * IAM; OpenAI / Anthropic read the lab's own API key from SSM at classify + * time. + * + * SSM paths for the API keys: + * `/easy-genomics/organization/{OrganizationId}/laboratory/{LaboratoryId}/llm-api-key-healthomics` + * `/easy-genomics/organization/{OrganizationId}/laboratory/{LaboratoryId}/llm-api-key-seqera` + * @enum {string} + */ + HealthOmicsLlmProvider?: "anthropic" | "bedrock" | "openai"; + HealthOmicsLlmModelId?: string; + /** @enum {string} */ + SeqeraLlmProvider?: "anthropic" | "bedrock" | "openai"; + SeqeraLlmModelId?: string; + /** + * @description When true, the failure classifier fetches the failed HealthOmics run's + * CloudWatch engine log, redacts PII + secrets, and sends a bounded excerpt to + * the configured LLM for deeper diagnosis. Requires a HealthOmics LLM provider. + */ + HealthOmicsLogEnrichmentEnabled?: boolean; + /** @description Boolean indicators returned by read-laboratory; the actual keys never leave SSM. */ + HasHealthOmicsLlmApiKey?: boolean; + HasSeqeraLlmApiKey?: boolean; CreatedAt?: string; CreatedBy?: string; ModifiedAt?: string; @@ -1024,8 +1061,18 @@ export interface components { NextFlowTowerWorkspaceId?: string; RunRetentionMonths?: number; EnableNewWorkflowsByDefault?: boolean; + /** @enum {string} */ + HealthOmicsLlmProvider?: "anthropic" | "bedrock" | "openai"; + HealthOmicsLlmModelId?: string; + /** @enum {string} */ + SeqeraLlmProvider?: "anthropic" | "bedrock" | "openai"; + SeqeraLlmModelId?: string; + HealthOmicsLogEnrichmentEnabled?: boolean; HasNextFlowTowerAccessToken?: boolean; HasGitHubAccessToken?: boolean; + /** @description Boolean indicators. The actual keys live in SSM and are never returned. */ + HasHealthOmicsLlmApiKey?: boolean; + HasSeqeraLlmApiKey?: boolean; }; RequestLaboratoryRequest: { /** Format: uuid */ @@ -1107,6 +1154,41 @@ export interface components { * or AWS HealthOmics `stopTime - startTime`. */ RunDurationSeconds?: number; + /** + * @description Top-level failure reason reported by the platform when a run reaches FAILED state. + * Sourced from HealthOmics `failureReason` or Seqera `workflow.errorMessage`. + * Absent on runs that failed before this field was introduced. + */ + FailureReason?: string; + /** + * @description HealthOmics human-readable `statusMessage` (often carries the failing task name + * and a CloudWatch log link). Kept separate from the machine-code `FailureReason` + * so the classifier receives both signals. + */ + FailureStatusMessage?: string; + /** + * @description Seqera `workflow.errorReport` — the richer Nextflow stack-trace / error detail, + * distinct from the one-line `workflow.errorMessage` stored in `FailureReason`. + */ + FailureErrorReport?: string; + /** + * @description Party responsible for resolving a failure: Lab user (input/data), Bioinformatician + * (workflow definition), AWS (transient, retry), or Ambiguous (needs investigation). + * Populated asynchronously by the classification pipeline after a FAILED transition. + * @enum {string} + */ + FailureOwner?: "AWS" | "Ambiguous" | "Bioinformatician" | "Lab"; + /** @description One-line human summary of the failure suitable for inline display. */ + FailureSummary?: string; + /** @description Suggested next step, imperative voice (e.g. "Increase memory allocation"). */ + FailureAction?: string; + /** + * @description Provenance of the classification: `lookup` = deterministic table hit, + * `llm` = produced by the configured LLM provider. Used to render an + * AI-assisted disclaimer in the UI and for ops debugging. + * @enum {string} + */ + FailureClassifiedBy?: "llm" | "lookup"; }; ReadLaboratoryRun: { LaboratoryId: string; @@ -1135,6 +1217,15 @@ export interface components { TerminalAt?: string; ExpiresAt?: number; RunDurationSeconds?: number; + FailureReason?: string; + FailureStatusMessage?: string; + FailureErrorReport?: string; + /** @enum {string} */ + FailureOwner?: "AWS" | "Ambiguous" | "Bioinformatician" | "Lab"; + FailureSummary?: string; + FailureAction?: string; + /** @enum {string} */ + FailureClassifiedBy?: "llm" | "lookup"; }; UpdateLaboratoryRunRequest: { Status: string; @@ -1159,6 +1250,15 @@ export interface components { NextFlowTowerWorkspaceId?: string; RunRetentionMonths?: number; EnableNewWorkflowsByDefault?: boolean; + /** @enum {string} */ + HealthOmicsLlmProvider?: "bedrock" | "openai" | "anthropic"; + HealthOmicsLlmModelId?: string; + /** @enum {string} */ + SeqeraLlmProvider?: "bedrock" | "openai" | "anthropic"; + SeqeraLlmModelId?: string; + HealthOmicsLogEnrichmentEnabled?: boolean; + HealthOmicsLlmApiKey?: string; + SeqeraLlmApiKey?: string; }; AddLaboratoryUserRequest: { /** Format: uuid */ @@ -1420,6 +1520,8 @@ export interface components { SampleIdSplitPattern?: string; OmicsWorkflowDefaultParams?: Record; FavouriteWorkflows?: components["schemas"]["FavouriteWorkflow"][]; + /** @enum {string} */ + AnalyticsConsent?: "denied" | "granted" | "unset"; CreatedAt?: string; CreatedBy?: string; ModifiedAt?: string; @@ -1447,6 +1549,8 @@ export interface components { Platform: "Seqera Cloud" | "AWS HealthOmics"; LaboratoryId: string; })[]; + /** @enum {string} */ + AnalyticsConsent?: "unset" | "granted" | "denied"; }; ListComputeEnvsResponse: { computeEnvs?: ({ diff --git a/projenrc/github-actions-api-diff-check.ts b/projenrc/github-actions-api-diff-check.ts index f6ea757a8..75a2fedab 100644 --- a/projenrc/github-actions-api-diff-check.ts +++ b/projenrc/github-actions-api-diff-check.ts @@ -40,6 +40,21 @@ export class GithubActionsApiDiffCheck extends Component { with: { 'node-version': project.minNodeVersion, cache: 'pnpm' }, }, { name: 'Install dependencies', run: 'pnpm install --frozen-lockfile' }, + { + name: 'Verify OpenAPI spec is up to date', + run: [ + 'set -euo pipefail', + '# Regenerate the spec + types from the Zod schemas and fail if the committed copy is stale', + '# (e.g. a schema change was committed with --no-verify, bypassing the pre-commit hook).', + 'pnpm --filter @easy-genomics/shared-lib run generate:openapi', + 'pnpm --filter @easy-genomics/shared-lib run generate:api-types', + 'if ! git diff --quiet -- packages/shared-lib/src/app/openapi/easy-genomics-api.yaml packages/shared-lib/src/app/types/easy-genomics/generated.d.ts; then', + ' echo "::error::OpenAPI spec/types are out of date. Run \'pnpm --filter @easy-genomics/shared-lib run generate:openapi && pnpm --filter @easy-genomics/shared-lib run generate:api-types\' and commit the result."', + ' git --no-pager diff -- packages/shared-lib/src/app/openapi/easy-genomics-api.yaml packages/shared-lib/src/app/types/easy-genomics/generated.d.ts', + ' exit 1', + 'fi', + ].join('\n'), + }, { id: 'diff', name: 'Run optic diff', diff --git a/projenrc/husky.ts b/projenrc/husky.ts index b3fb287a2..12d9e9d13 100644 --- a/projenrc/husky.ts +++ b/projenrc/husky.ts @@ -32,6 +32,13 @@ export class Husky extends Component { ); preCommit.addLine('pnpm pre-commit'); preCommit.addLine('pnpm exec projen'); + // Keep the OpenAPI spec + generated API types in sync with the Zod schemas. + // Regenerate and stage them so a schema change can never be committed with a stale spec. + preCommit.addLine('pnpm --filter @easy-genomics/shared-lib run generate:openapi'); + preCommit.addLine('pnpm --filter @easy-genomics/shared-lib run generate:api-types'); + preCommit.addLine( + 'git add packages/shared-lib/src/app/openapi/easy-genomics-api.yaml packages/shared-lib/src/app/types/easy-genomics/generated.d.ts', + ); preCommit.addLine('pnpm --filter @easy-genomics/back-end test -- --silent'); preCommit.addLine(''); }