using System.Text.Json; namespace AIStudio.Assistants.VisualBriefing; internal sealed partial class VisualBriefingBuildOrchestrator { /// /// Recompiles one immutable revision with the current deterministic export pipeline without /// accessing sources or calling a model. /// /// The current local briefing manifest. /// The revision whose semantic artifacts are reused. /// The cancellation token. /// The terminal recompile result. public async Task RecompileAsync(VisualBriefingManifest manifest, Guid parentRevisionId, CancellationToken token = default) { var operationId = Guid.NewGuid(); var proposedBuildId = Guid.NewGuid(); var diagnostics = new VisualBriefingOperationDiagnostics { OperationId = operationId, BuildId = proposedBuildId, Stage = VisualBriefingBuildStage.COMPILATION, StartedAtUtc = DateTimeOffset.UtcNow, }; this.liveDiagnostics[manifest.BriefingId] = diagnostics; var gate = this.buildLocks.GetOrAdd(manifest.BriefingId, _ => new(1, 1)); await gate.WaitAsync(token); VisualBriefingBuildRecord? build = null; try { var parent = await this.LoadParentContextAsync(manifest, VisualBriefingEditMode.RECOMPILE, parentRevisionId, token); if (parent is not { ParentVersion: { } parentVersion, Parts: { } parentParts, Evidence: { } evidence, Plan: { } plan, Content: { } content, Presentation: { } previousPresentation, }) throw new VisualBriefingBuildException( VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED, VisualBriefingBuildStage.COMPILATION, "This briefing version cannot be recompiled with the current AI Studio version. Rebuild the briefing instead.", "The selected revision does not contain a complete compatible set of semantic artifacts."); var inputFingerprint = VisualBriefingHashing.ComputeSections( parentRevisionId.ToString("D"), evidence.PayloadHash, plan.PayloadHash, content.PayloadHash, previousPresentation.PayloadHash, parentVersion.AssetHash, VisualBriefingVersions.COMPILER.ToString(), VisualBriefingVersions.SCHEMA.ToString(), VisualBriefingVersions.RUNTIME.ToString()); var now = DateTimeOffset.UtcNow; var candidate = new VisualBriefingBuildRecord { BuildId = proposedBuildId, OperationId = operationId, BriefingId = manifest.BriefingId, Mode = VisualBriefingEditMode.RECOMPILE, ParentRevisionId = parentRevisionId, InputFingerprint = inputFingerprint, SourceFingerprint = parentVersion.AssetHash, CreatedAtUtc = now, UpdatedAtUtc = now, EvidenceArtifactId = evidence.ArtifactId, PlanArtifactId = plan.ArtifactId, ContentArtifactId = content.ArtifactId, Stages = [ .. Enum.GetValues().Select(stage => new VisualBriefingBuildStageRecord { Stage = stage }) ], }; var selectedBuild = await this.store.StartOrResumeBuildAsync(candidate, token); build = selectedBuild.Build; build.OperationId = operationId; diagnostics.BuildId = build.BuildId; MarkSkipped(build, VisualBriefingBuildStage.SOURCE_PREPARATION, parentVersion.AssetHash); MarkSkipped(build, VisualBriefingBuildStage.EVIDENCE, evidence.PayloadHash); MarkSkipped(build, VisualBriefingBuildStage.PLAN, plan.PayloadHash); MarkSkipped(build, VisualBriefingBuildStage.CONTENT, content.PayloadHash); MarkSkipped(build, VisualBriefingBuildStage.DESIGN, previousPresentation.PayloadHash); await this.store.SaveBuildAsync(build, token); this.progressService.Publish(build); diagnostics.ContentHashes["evidence"] = evidence.PayloadHash; diagnostics.ContentHashes["plan"] = plan.PayloadHash; diagnostics.ContentHashes["content"] = content.PayloadHash; diagnostics.ArtifactIds["evidence"] = evidence.ArtifactId; diagnostics.ArtifactIds["plan"] = plan.ArtifactId; diagnostics.ArtifactIds["content"] = content.ArtifactId; diagnostics.Stage = VisualBriefingBuildStage.COMPILATION; var compilationStage = GetStage(build, VisualBriefingBuildStage.COMPILATION); compilationStage.Status = VisualBriefingBuildStageStatus.RUNNING; compilationStage.StartedAtUtc = DateTimeOffset.UtcNow; compilationStage.FinishedAtUtc = null; compilationStage.Failure = null; compilationStage.InputFingerprint = inputFingerprint; build.UpdatedAtUtc = DateTimeOffset.UtcNow; await this.store.SaveBuildAsync(build, token); this.progressService.Publish(build); var compiled = VisualBriefingCompilerInvariant.Guard( VisualBriefingBuildStage.COMPILATION, () => VisualBriefingLayoutCompiler.Compile( plan, content, previousPresentation.Layout, previousPresentation.Profile)); var validationDataProperties = compiled.Data.EnumerateObject() .ToDictionary(property => property.Name, property => property.Value.Clone(), StringComparer.Ordinal); validationDataProperties["_mwai"] = JsonSerializer.SerializeToElement(new { schemaVersion = VisualBriefingVersions.SCHEMA, runtimeVersion = VisualBriefingVersions.RUNTIME, aiStudioVersion = "validation", assets = content.AssetPlan.ToDictionary(asset => asset.AssetId, _ => "data:image/png;base64,AA==", StringComparer.Ordinal), footer = new { createdWith = "validation", models = "validation", createdAt = "validation", authors = "validation", protection = "validation", }, }, VisualBriefingJson.Canonical); VisualBriefingCompilerInvariant.Guard( VisualBriefingBuildStage.COMPILATION, VisualBriefingArtifactService.ValidateGeneratedParts(manifest, JsonSerializer.SerializeToElement(validationDataProperties, VisualBriefingJson.Canonical), compiled.TemplateHtml, compiled.Css, content.Charts.Count > 0)); var contributions = await this.ResolveRecompileModelContributionsAsync(manifest.BriefingId, parentVersion, evidence, plan, content, previousPresentation, token); var presentationModel = contributions.First(contribution => contribution.Role is VisualBriefingModelRole.DESIGN).Model; var presentation = new VisualBriefingPresentationArtifact { ArtifactId = Guid.NewGuid(), CreatedAtUtc = DateTimeOffset.UtcNow, PayloadHash = VisualBriefingPayloadHash.ForPresentation(previousPresentation.Layout, previousPresentation.Profile, compiled.TemplateHash, compiled.CssHash), Layout = previousPresentation.Layout, Profile = previousPresentation.Profile, TemplateHtml = compiled.TemplateHtml, Css = compiled.Css, TemplateHash = compiled.TemplateHash, CssHash = compiled.CssHash, Model = presentationModel, }; await this.store.WritePresentationArtifactAsync(manifest.BriefingId, presentation, token); build.PresentationArtifactId = presentation.ArtifactId; diagnostics.ContentHashes["design"] = presentation.PayloadHash; diagnostics.ArtifactIds["design"] = presentation.ArtifactId; compilationStage.Status = VisualBriefingBuildStageStatus.COMPLETED; compilationStage.FinishedAtUtc = DateTimeOffset.UtcNow; compilationStage.OutputHash = VisualBriefingHashing.ComputeSections( VisualBriefingHashing.Compute(VisualBriefingHashing.CanonicalJson(compiled.Data)), compiled.TemplateHash, compiled.CssHash); build.UpdatedAtUtc = DateTimeOffset.UtcNow; await this.store.SaveBuildAsync(build, token); this.progressService.Publish(build); diagnostics.Stage = VisualBriefingBuildStage.ASSEMBLY; var revisionId = build.RevisionId ?? Guid.NewGuid(); var revisionCreatedAt = DateTimeOffset.UtcNow; build.RevisionId = revisionId; var assemblyStage = GetStage(build, VisualBriefingBuildStage.ASSEMBLY); assemblyStage.Status = VisualBriefingBuildStageStatus.RUNNING; assemblyStage.StartedAtUtc = revisionCreatedAt; assemblyStage.FinishedAtUtc = null; assemblyStage.Failure = null; assemblyStage.InputFingerprint = VisualBriefingHashing.ComputeSections( content.PayloadHash, presentation.PayloadHash, parentVersion.AssetHash, VisualBriefingVersions.ARTIFACT.ToString(), VisualBriefingVersions.COMPILER.ToString(), VisualBriefingVersions.SCHEMA.ToString(), VisualBriefingVersions.RUNTIME.ToString()); build.UpdatedAtUtc = DateTimeOffset.UtcNow; await this.store.SaveBuildAsync(build, token); this.progressService.Publish(build); var revision = await this.store.AddRevisionAsync(new( manifest.BriefingId, parentRevisionId, VisualBriefingEditMode.RECOMPILE, string.Empty, compiled.Data, compiled.TemplateHtml, compiled.Css, string.Empty, "MindWork AI Studio", content.ArtifactId, presentation.ArtifactId, build.BuildId, build.OperationId, contributions, revisionId, revisionCreatedAt, VisualBriefingData.ExtractAssets(parentParts.Data), content.AssetPlan, evidence.ArtifactId, plan.ArtifactId, parentParts.ExportManifest), token); var commitStage = GetStage(build, VisualBriefingBuildStage.COMMIT); if (!revision.Success || revision.Version is null) throw new VisualBriefingBuildException(VisualBriefingFailureCode.STORE_FAILED, VisualBriefingBuildStage.COMMIT, revision.Issue, $"The immutable recompiled revision commit was rejected. StoreIssue={revision.Issue}"); assemblyStage.Status = VisualBriefingBuildStageStatus.COMPLETED; assemblyStage.FinishedAtUtc = DateTimeOffset.UtcNow; assemblyStage.OutputHash = revision.Version.DocumentHash; commitStage.Status = VisualBriefingBuildStageStatus.COMPLETED; commitStage.StartedAtUtc = assemblyStage.FinishedAtUtc; commitStage.FinishedAtUtc = DateTimeOffset.UtcNow; commitStage.InputFingerprint = revision.Version.DocumentHash; commitStage.OutputHash = revision.Version.DocumentHash; build.CommittedRevisionId = revision.Version.RevisionId; build.Status = VisualBriefingBuildStatus.COMPLETED; build.Failure = null; build.UpdatedAtUtc = DateTimeOffset.UtcNow; await this.store.SaveBuildAsync(build, token); this.progressService.Publish(build); diagnostics.ContentHashes["document"] = revision.Version.DocumentHash; diagnostics.FinishedAtUtc = DateTimeOffset.UtcNow; return new( true, revision.Version, string.Empty, VisualBriefingFailureCode.NONE, diagnostics, false); } catch (OperationCanceledException) { var failure = new VisualBriefingFailure { Code = VisualBriefingFailureCode.CANCELED, Stage = diagnostics.Stage, UserMessage = "The visual briefing recompilation was canceled.", TechnicalDetails = "The operation cancellation token was signaled.", }; if (build is not null) await this.SaveTerminalStateAsync(build, VisualBriefingBuildStatus.CANCELED, failure, CancellationToken.None); return FinishFailure(diagnostics, build, failure, canContinueAsRebuild: false); } catch (VisualBriefingBuildException exception) { var failure = new VisualBriefingFailure { Code = exception.Code, Stage = exception.Stage, ValidationRule = exception.Stage is VisualBriefingBuildStage.COMPILATION ? VisualBriefingValidationRule.COMPILER_OUTPUT_INVALID : VisualBriefingValidationRule.NONE, UserMessage = exception.Message, TechnicalDetails = exception.TechnicalDetails, }; if (build is not null) await this.SaveTerminalStateAsync(build, VisualBriefingBuildStatus.FAILED, failure, CancellationToken.None); return FinishFailure(diagnostics, build, failure, canContinueAsRebuild: false); } catch (Exception exception) { var failure = new VisualBriefingFailure { Code = VisualBriefingFailureCode.UNEXPECTED, Stage = diagnostics.Stage, UserMessage = "The visual briefing could not be recompiled because of an unexpected internal error.", TechnicalDetails = $"{exception.GetType().Name} at stage {diagnostics.Stage}.", }; if (build is not null) await this.SaveTerminalStateAsync(build, VisualBriefingBuildStatus.FAILED, failure, CancellationToken.None); return FinishFailure(diagnostics, build, failure, canContinueAsRebuild: false); } finally { gate.Release(); } } /// /// Reconstructs the most specific model attribution available for each reused semantic artifact. /// private async Task> ResolveRecompileModelContributionsAsync(Guid briefingId, VisualBriefingVersion parentVersion, VisualBriefingEvidenceArtifact evidence, VisualBriefingPlanArtifact plan, VisualBriefingContentArtifact content, VisualBriefingPresentationArtifact presentation, CancellationToken token) { var builds = await this.store.ListBuildsAsync(briefingId, token); return [ new( VisualBriefingModelRole.EVIDENCE, ResolveRecompileModelLabel( builds, build => build.EvidenceArtifactId, evidence.ArtifactId, VisualBriefingBuildStage.EVIDENCE, ExistingModelLabel(parentVersion, VisualBriefingModelRole.EVIDENCE, evidence.Model))), new( VisualBriefingModelRole.PLAN, ResolveRecompileModelLabel( builds, build => build.PlanArtifactId, plan.ArtifactId, VisualBriefingBuildStage.PLAN, ExistingModelLabel(parentVersion, VisualBriefingModelRole.PLAN, plan.Model))), new( VisualBriefingModelRole.CONTENT, ResolveRecompileModelLabel( builds, build => build.ContentArtifactId, content.ArtifactId, VisualBriefingBuildStage.CONTENT, ExistingModelLabel(parentVersion, VisualBriefingModelRole.CONTENT, content.Model))), new( VisualBriefingModelRole.DESIGN, ResolveRecompileModelLabel( builds, build => build.PresentationArtifactId, presentation.ArtifactId, VisualBriefingBuildStage.DESIGN, ExistingModelLabel(parentVersion, VisualBriefingModelRole.DESIGN, presentation.Model))), ]; } /// /// Resolves the provider and model that originally produced one immutable artifact. /// private static string ResolveRecompileModelLabel(IReadOnlyList builds, Func artifactId, Guid expectedArtifactId, VisualBriefingBuildStage stage, string fallback) { var producingBuild = builds.FirstOrDefault(build => artifactId(build) == expectedArtifactId && !string.IsNullOrWhiteSpace(build.ProviderFamily) && !string.IsNullOrWhiteSpace(build.Model) && build.Stages.Any(candidate => candidate.Stage == stage && candidate.Status is VisualBriefingBuildStageStatus.COMPLETED)); return producingBuild is null ? fallback : VisualBriefingModelNames.ExportLabel(producingBuild.ProviderFamily, producingBuild.Model); } /// /// Returns the persisted role attribution, falling back to the immutable artifact label. /// private static string ExistingModelLabel(VisualBriefingVersion parentVersion, VisualBriefingModelRole role, string artifactModel) { var contribution = parentVersion.ModelContributions.FirstOrDefault(candidate => candidate.Role == role && !string.IsNullOrWhiteSpace(candidate.Model)); return contribution?.Model ?? artifactModel; } }