Centralize summary frontmatter metadata
PR and Push Build/Test / build-and-test (push) Successful in 9m47s

This commit is contained in:
2026-05-27 12:55:20 +02:00
parent 4f0b52d7d0
commit c12febe029
4 changed files with 96 additions and 53 deletions
@@ -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);
@@ -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(
@@ -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<MeetingArtifactFrontmatter> 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<List<string>?> ResolveSummaryAttendeesAsync(
string summaryPath,
MeetingNote meetingNote,
CancellationToken cancellationToken)
{
var existingAttendees = await ReadExistingSummaryAttendeesAsync(summaryPath, cancellationToken);
return existingAttendees ?? CopyNonEmptyList(meetingNote.Frontmatter.Attendees);
}
private static List<string>? CopyNonEmptyList(IReadOnlyCollection<string> values)
{
return values.Count == 0 ? null : values.ToList();
}
private static async Task<List<string>?> 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<SummaryFrontmatterYaml>(document.Frontmatter);
return yaml?.Attendees;
}
private sealed class SummaryFrontmatterYaml
{
[YamlMember(Alias = "attendees")]
public List<string>? Attendees { get; set; }
}
}
@@ -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<List<string>?> 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<string>? ResolveSummaryProjects(MeetingNote meetingNote)
{
return meetingNote.Frontmatter.Projects.Count == 0
? null
: meetingNote.Frontmatter.Projects.ToList();
}
private async Task<List<string>?> 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<SummaryFrontmatterYaml>(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<string>? Attendees { get; set; }
}
private static DateTimeOffset? ParseDateTime(string? value)
{
return DateTimeOffset.TryParse(value, out var parsed)