using AIStudio.Provider; using AIStudio.Settings; using AIStudio.Tools.Rust; using ProviderSettings = AIStudio.Settings.Provider; namespace AIStudio.Assistants.VisualBriefing; internal sealed partial class VisualBriefingBuildOrchestrator { /// /// Loads and verifies the selected parent revision and its intermediate artifacts. /// /// The briefing manifest. /// The edit mode. /// The parent revision identifier. /// The cancellation token. /// The parent context. private async Task LoadParentContextAsync( VisualBriefingManifest manifest, VisualBriefingEditMode mode, Guid? parentRevisionId, CancellationToken token) { if (mode is VisualBriefingEditMode.INITIAL) return new(null, null, null, null, null, null); if (parentRevisionId is null) throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.SOURCE_PREPARATION, mode is VisualBriefingEditMode.RECOMPILE ? "This briefing version cannot be recompiled with the current AI Studio version. Rebuild the briefing instead." : "The selected parent revision could not be loaded.", "A non-initial build has no parent revision ID."); var version = manifest.Versions.FirstOrDefault(candidate => candidate.RevisionId == parentRevisionId); if (mode is VisualBriefingEditMode.REBUILD) return version is not null ? new(version, null, null, null, null, null) : throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.SOURCE_PREPARATION, "The selected parent revision could not be loaded.", "The rebuild parent revision does not exist."); var parts = mode is VisualBriefingEditMode.RECOMPILE ? await this.store.ReadVersionPartsForRecompileAsync(manifest.BriefingId, parentRevisionId.Value, token) : await this.store.ReadVersionPartsAsync(manifest.BriefingId, parentRevisionId.Value, token); if (version is null || parts is null || version.EvidenceArtifactId is null || version.PlanArtifactId is null || version.ContentArtifactId is null || version.PresentationArtifactId is null) throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.SOURCE_PREPARATION, mode is VisualBriefingEditMode.RECOMPILE ? "This briefing version cannot be recompiled with the current AI Studio version. Rebuild the briefing instead." : "The selected parent revision is invalid or incomplete.", "The parent revision or its intermediate artifact references are unavailable."); var evidence = await this.store.ReadEvidenceArtifactAsync( manifest.BriefingId, version.EvidenceArtifactId.Value, token); var plan = await this.store.ReadPlanArtifactAsync( manifest.BriefingId, version.PlanArtifactId.Value, token); var content = await this.store.ReadContentArtifactAsync( manifest.BriefingId, version.ContentArtifactId.Value, token); var presentation = await this.store.ReadPresentationArtifactAsync( manifest.BriefingId, version.PresentationArtifactId.Value, token); if (evidence is null || plan is null || content is null || presentation is null) throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.SOURCE_PREPARATION, mode is VisualBriefingEditMode.RECOMPILE ? "This briefing version cannot be recompiled with the current AI Studio version. Rebuild the briefing instead." : "The selected parent revision has damaged intermediate artifacts.", "A referenced evidence, plan, content, or design artifact failed hash validation."); return new(version, parts, evidence, plan, content, presentation); } /// /// Loads validated evidence for the explicit continue-as-rebuild action. /// /// The briefing identifier. /// The source build identifier. /// The cancellation token. /// The reusable evidence artifact. private async Task<(VisualBriefingEvidenceArtifact Evidence, string SourceFingerprint, string InputFingerprint)> LoadReusableEvidenceAsync( Guid briefingId, Guid buildId, CancellationToken token) { var sourceBuild = await this.store.LoadBuildAsync(briefingId, buildId, token); if (sourceBuild is null || sourceBuild.Status is not VisualBriefingBuildStatus.AWAITING_REBUILD || sourceBuild.EvidenceArtifactId is null) throw new VisualBriefingBuildException( VisualBriefingFailureCode.CONTENT_SIGNATURE_INCOMPATIBLE, VisualBriefingBuildStage.EVIDENCE, "The validated evidence is no longer available to continue as a rebuild.", "The source build is not awaiting rebuild or has no evidence artifact."); var evidence = await this.store.ReadEvidenceArtifactAsync( briefingId, sourceBuild.EvidenceArtifactId.Value, token) ?? throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.EVIDENCE, "The validated evidence artifact is damaged.", "The reusable evidence artifact failed hash validation."); var persistedEvidenceStage = sourceBuild.Stages.FirstOrDefault(stage => stage.Stage is VisualBriefingBuildStage.EVIDENCE && stage.Status is VisualBriefingBuildStageStatus.COMPLETED); if (persistedEvidenceStage is null || string.IsNullOrWhiteSpace(persistedEvidenceStage.InputFingerprint)) throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.EVIDENCE, "The validated evidence dependencies are unavailable.", "The reusable evidence stage has no validated input fingerprint."); return (evidence, sourceBuild.SourceFingerprint, persistedEvidenceStage.InputFingerprint); } /// /// Computes a current source fingerprint including persistent transcript hashes. /// /// The briefing manifest. /// The cancellation token. /// The current source fingerprint. private async Task ComputeCurrentSourceFingerprintAsync( VisualBriefingManifest manifest, CancellationToken token) { List entries = []; foreach (var source in manifest.Sources.OrderBy(source => source.SourceId)) { token.ThrowIfCancellationRequested(); if (!File.Exists(source.Path)) throw new VisualBriefingBuildException( VisualBriefingFailureCode.SOURCE_UNREACHABLE, VisualBriefingBuildStage.SOURCE_PREPARATION, "A briefing source is no longer reachable.", $"Source {source.SourceId:D} failed the reachability check."); var sourceHash = await VisualBriefingHashing.ComputeFileAsync(source.Path, token); var transcriptHash = string.Empty; if (source.IsMedia) { var transcript = await this.store.ReadTranscriptAsync(manifest.BriefingId, source.SourceId, token); if (string.IsNullOrWhiteSpace(transcript) || source.TranscriptStatus is not VisualBriefingTranscriptStatus.CURRENT) throw new VisualBriefingBuildException( VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE, VisualBriefingBuildStage.SOURCE_PREPARATION, "A media transcript is missing or outdated.", $"Transcript status for source {source.SourceId:D} is {source.TranscriptStatus}."); transcriptHash = VisualBriefingHashing.Compute(transcript); } entries.Add(string.Join( '\u001f', source.SourceId, source.Kind, source.AssetId, sourceHash, transcriptHash)); } return VisualBriefingHashing.ComputeSections( [manifest.Settings.OptimizeImages.ToString(), .. entries]); } /// /// Computes the full safe build input fingerprint. /// /// The briefing manifest. /// The edit mode. /// The parent revision. /// The provider. /// The profile. /// The source fingerprint. /// The optional reused content hash. /// The build input fingerprint. private static string ComputeBuildInputFingerprint( VisualBriefingManifest manifest, VisualBriefingEditMode mode, Guid? parentRevisionId, ProviderSettings provider, Profile profile, string sourceFingerprint, string? reusedContentHash) => VisualBriefingHashing.ComputeSections( mode.ToString(), parentRevisionId?.ToString("D"), provider.Id, provider.Model.Id, profile.Id, sourceFingerprint, VisualBriefingHashing.Compute(manifest.Settings.Instruction), manifest.Settings.TargetLanguage.ToString(), manifest.Settings.CustomTargetLanguage, manifest.Settings.AudienceProfile.ToString(), manifest.Settings.AudienceAgeGroup.ToString(), manifest.Settings.AudienceOrganizationalLevel.ToString(), manifest.Settings.AudienceExpertise.ToString(), manifest.Settings.ShowSourceReferences.ToString(), manifest.Settings.OptimizeImages.ToString(), manifest.Settings.ProtectionLevel.ToString(), VisualBriefingHashing.Compute(manifest.Settings.CustomProtectionLevel), reusedContentHash, VisualBriefingVersions.EVIDENCE_CONTRACT.ToString(), VisualBriefingVersions.PLAN_CONTRACT.ToString(), VisualBriefingVersions.CONTENT_CONTRACT.ToString(), VisualBriefingVersions.DESIGN_CONTRACT.ToString(), VisualBriefingVersions.COMPILER.ToString(), VisualBriefingVersions.SCHEMA.ToString(), VisualBriefingVersions.RUNTIME.ToString()); /// /// Validates the selected provider. /// /// The provider. private static void ValidateProvider(ProviderSettings provider) { if (provider == ProviderSettings.NONE || provider.UsedLLMProvider is LLMProviders.NONE) throw new VisualBriefingBuildException( VisualBriefingFailureCode.PROVIDER_NOT_SELECTED, VisualBriefingBuildStage.SOURCE_PREPARATION, "Please select an LLM provider.", "No provider is selected."); } /// /// Ensures content-generating builds have at least one source-material file. /// /// The briefing manifest. /// The requested edit mode. private static void ValidateSourceMaterial(VisualBriefingManifest manifest, VisualBriefingEditMode mode) { if (mode is VisualBriefingEditMode.CHANGE_DESIGN or VisualBriefingEditMode.RECOMPILE || manifest.Sources.Any(source => source.Kind is VisualBriefingSourceKind.SOURCE_MATERIAL)) { return; } throw new VisualBriefingBuildException( VisualBriefingFailureCode.SOURCE_PREPARATION_FAILED, VisualBriefingBuildStage.SOURCE_PREPARATION, "Please add at least one source material file.", "The briefing has no SOURCE_MATERIAL source."); } /// /// Validates image-input capabilities for content analysis. /// /// The briefing manifest. /// The provider. private static void ValidateVisionCapabilities( VisualBriefingManifest manifest, ProviderSettings provider) { var imageSources = manifest.Sources.Where(source => source.Kind is VisualBriefingSourceKind.VISUAL_ASSET || FileTypes.IsAllowedPath(source.Path, FileTypes.IMAGE)).ToArray(); if (imageSources.Length == 0) return; var capabilities = provider.GetModelCapabilities(); var acceptsImages = imageSources.Length == 1 ? capabilities.Contains(Capability.SINGLE_IMAGE_INPUT) || capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT) : capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT); if (!acceptsImages) throw new VisualBriefingBuildException( VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING, VisualBriefingBuildStage.SOURCE_PREPARATION, "The selected model cannot process the number of source images and visual assets.", $"ImageCount={imageSources.Length}; SingleImage={capabilities.Contains(Capability.SINGLE_IMAGE_INPUT)}; MultipleImages={capabilities.Contains(Capability.MULTIPLE_IMAGE_INPUT)}."); } /// /// Groups validated parent-revision inputs. /// /// The local version metadata. /// The parsed standalone artifact. /// The content artifact. /// The presentation artifact. private sealed record ParentContext( VisualBriefingVersion? ParentVersion, VisualBriefingArtifactParts? Parts, VisualBriefingEvidenceArtifact? Evidence, VisualBriefingPlanArtifact? Plan, VisualBriefingContentArtifact? Content, VisualBriefingPresentationArtifact? Presentation); }