diff --git a/MeetingAssistant.Tests/MeetingSummaryFailureWriterTests.cs b/MeetingAssistant.Tests/MeetingSummaryFailureWriterTests.cs index d930970..b0dca8a 100644 --- a/MeetingAssistant.Tests/MeetingSummaryFailureWriterTests.cs +++ b/MeetingAssistant.Tests/MeetingSummaryFailureWriterTests.cs @@ -24,14 +24,27 @@ public sealed class MeetingSummaryFailureWriterTests title: Failure Meeting start_time: "2026-05-20T10:00:00.0000000+02:00" end_time: "2026-05-20T10:30:00.0000000+02:00" - attendees: [] - projects: [] + attendees: + - Ada + projects: + - Current Project transcript: "[[../Transcripts/transcript|Transcript]]" assistant_context: "[[../Assistant Context/context|Assistant Context]]" summary: "[[../Summaries/summary|Summary]]" --- """); - await File.WriteAllTextAsync(artifacts.SummaryPath, "# Old Summary"); + await File.WriteAllTextAsync( + artifacts.SummaryPath, + """ + --- + attendees: + - Preserved + projects: + - Stale Project + --- + + # Old Summary + """); var writer = new MeetingSummaryFailureWriter( Options.Create(new MeetingAssistantOptions { @@ -54,6 +67,12 @@ public sealed class MeetingSummaryFailureWriterTests Assert.Equal(artifacts.SummaryPath, result.SummaryPath); Assert.Contains("Summary Generation Failed", content); Assert.Contains("title: Failure Meeting", content); + Assert.Contains("attendees:", content); + Assert.Contains("- Preserved", content); + Assert.DoesNotContain("- Ada", content); + Assert.Contains("projects:", content); + Assert.Contains("- Current Project", content); + Assert.DoesNotContain("- Stale Project", content); Assert.Contains("meeting: \"[[../Notes/meeting|Meeting Note]]\"", content); Assert.Contains("transcript: \"[[../Transcripts/transcript|Transcript]]\"", content); Assert.Contains("assistant_context: \"[[../Assistant Context/context|Assistant Context]]\"", content); diff --git a/MeetingAssistant/Summary/MeetingSummaryFailureWriter.cs b/MeetingAssistant/Summary/MeetingSummaryFailureWriter.cs index 1fd1c1c..0e2f838 100644 --- a/MeetingAssistant/Summary/MeetingSummaryFailureWriter.cs +++ b/MeetingAssistant/Summary/MeetingSummaryFailureWriter.cs @@ -52,11 +52,11 @@ public sealed class MeetingSummaryFailureWriter : IMeetingSummaryFailureWriter var meetingNote = File.Exists(artifacts.MeetingNotePath) ? await meetingNoteStore.ReadAsync(artifacts.MeetingNotePath, cancellationToken) : new MeetingNote("", new MeetingNoteFrontmatter(), ""); - var frontmatter = MeetingArtifactFrontmatterRenderer.Create( + var frontmatter = await MeetingSummaryFrontmatterFactory.CreateAsync( artifacts, meetingNote, MeetingArtifactFrontmatterRenderer.DefaultTitle(meetingNote, "Meeting Summary"), - artifacts.SummaryPath); + cancellationToken); await File.WriteAllTextAsync( artifacts.SummaryPath, MeetingArtifactFrontmatterRenderer.Render( diff --git a/MeetingAssistant/Summary/MeetingSummaryFrontmatterFactory.cs b/MeetingAssistant/Summary/MeetingSummaryFrontmatterFactory.cs new file mode 100644 index 0000000..96798e0 --- /dev/null +++ b/MeetingAssistant/Summary/MeetingSummaryFrontmatterFactory.cs @@ -0,0 +1,70 @@ +using MeetingAssistant.MeetingNotes; +using YamlDotNet.Serialization; + +namespace MeetingAssistant.Summary; + +internal static class MeetingSummaryFrontmatterFactory +{ + private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .Build(); + + public static async Task CreateAsync( + MeetingSessionArtifacts artifacts, + MeetingNote meetingNote, + string title, + CancellationToken cancellationToken) + { + var frontmatter = MeetingArtifactFrontmatterRenderer.Create( + artifacts, + meetingNote, + title, + artifacts.SummaryPath); + frontmatter.Attendees = await ResolveSummaryAttendeesAsync( + artifacts.SummaryPath, + meetingNote, + cancellationToken); + frontmatter.Projects = CopyNonEmptyList(meetingNote.Frontmatter.Projects); + return frontmatter; + } + + private static async Task?> ResolveSummaryAttendeesAsync( + string summaryPath, + MeetingNote meetingNote, + CancellationToken cancellationToken) + { + var existingAttendees = await ReadExistingSummaryAttendeesAsync(summaryPath, cancellationToken); + return existingAttendees ?? CopyNonEmptyList(meetingNote.Frontmatter.Attendees); + } + + private static List? CopyNonEmptyList(IReadOnlyCollection values) + { + return values.Count == 0 ? null : values.ToList(); + } + + private static async Task?> ReadExistingSummaryAttendeesAsync( + string summaryPath, + CancellationToken cancellationToken) + { + if (!File.Exists(summaryPath)) + { + return null; + } + + var content = await File.ReadAllTextAsync(summaryPath, cancellationToken); + var document = MarkdownDocumentParser.SplitOptional(content); + if (!document.HasFrontmatter) + { + return null; + } + + var yaml = YamlDeserializer.Deserialize(document.Frontmatter); + return yaml?.Attendees; + } + + private sealed class SummaryFrontmatterYaml + { + [YamlMember(Alias = "attendees")] + public List? Attendees { get; set; } + } +} diff --git a/MeetingAssistant/Summary/MeetingSummaryTools.cs b/MeetingAssistant/Summary/MeetingSummaryTools.cs index 2a181bc..5d432ad 100644 --- a/MeetingAssistant/Summary/MeetingSummaryTools.cs +++ b/MeetingAssistant/Summary/MeetingSummaryTools.cs @@ -101,13 +101,11 @@ public sealed class MeetingSummaryTools var summaryTitle = string.IsNullOrWhiteSpace(meetingNote.Frontmatter.Title) ? title : meetingNote.Frontmatter.Title; - var frontmatter = MeetingArtifactFrontmatterRenderer.Create( + var frontmatter = await MeetingSummaryFrontmatterFactory.CreateAsync( artifacts, meetingNote, string.IsNullOrWhiteSpace(summaryTitle) ? "Meeting Summary" : summaryTitle, - artifacts.SummaryPath); - frontmatter.Attendees = await ResolveSummaryAttendeesAsync(meetingNote); - frontmatter.Projects = ResolveSummaryProjects(meetingNote); + CancellationToken.None); await File.WriteAllTextAsync( artifacts.SummaryPath, MeetingArtifactFrontmatterRenderer.Render(frontmatter, markdown)); @@ -579,44 +577,6 @@ public sealed class MeetingSummaryTools .Replace(Path.AltDirectorySeparatorChar.ToString(), "/", StringComparison.Ordinal); } - private async Task?> ResolveSummaryAttendeesAsync(MeetingNote meetingNote) - { - var existingAttendees = await ReadExistingSummaryAttendeesAsync(); - if (existingAttendees is not null) - { - return existingAttendees; - } - - return meetingNote.Frontmatter.Attendees.Count == 0 - ? null - : meetingNote.Frontmatter.Attendees.ToList(); - } - - private static List? ResolveSummaryProjects(MeetingNote meetingNote) - { - return meetingNote.Frontmatter.Projects.Count == 0 - ? null - : meetingNote.Frontmatter.Projects.ToList(); - } - - private async Task?> ReadExistingSummaryAttendeesAsync() - { - if (!File.Exists(artifacts.SummaryPath)) - { - return null; - } - - var content = await File.ReadAllTextAsync(artifacts.SummaryPath); - var document = MarkdownDocumentParser.SplitOptional(content); - if (!document.HasFrontmatter) - { - return null; - } - - var yaml = YamlDeserializer.Deserialize(document.Frontmatter); - return yaml?.Attendees; - } - private sealed record ProjectFileTarget(BoundMeetingProject Project, string Path); private sealed record FileLineEditMode( @@ -679,12 +639,6 @@ public sealed class MeetingSummaryTools public string? Summary { get; set; } } - private sealed class SummaryFrontmatterYaml - { - [YamlDotNet.Serialization.YamlMember(Alias = "attendees")] - public List? Attendees { get; set; } - } - private static DateTimeOffset? ParseDateTime(string? value) { return DateTimeOffset.TryParse(value, out var parsed)