Public Access
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1afc1e14f9 |
@@ -105,36 +105,6 @@ public sealed class MeetingNoteStoreTests
|
|||||||
Assert.Equal(userNotes, reloaded.UserNotes);
|
Assert.Equal(userNotes, reloaded.UserNotes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ReadConvertsScalarListFrontmatterToSingleItemLists()
|
|
||||||
{
|
|
||||||
var (vaultRoot, store) = CreateStore();
|
|
||||||
var notePath = Path.Combine(vaultRoot, "Meetings", "Notes", "manual.md");
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(notePath)!);
|
|
||||||
await File.WriteAllTextAsync(
|
|
||||||
notePath,
|
|
||||||
"""
|
|
||||||
---
|
|
||||||
title: Manual frontmatter
|
|
||||||
start_time: "2026-05-19T10:00:00.0000000+02:00"
|
|
||||||
end_time: ""
|
|
||||||
attendees: Ada
|
|
||||||
projects: Meeting Assistant
|
|
||||||
transcript: "[[../Transcripts/manual-transcript|Transcript]]"
|
|
||||||
assistant_context: "[[../Assistant Context/manual-context|Assistant Context]]"
|
|
||||||
summary: "[[../Summaries/manual-summary|Summary]]"
|
|
||||||
---
|
|
||||||
|
|
||||||
User notes.
|
|
||||||
""");
|
|
||||||
|
|
||||||
var loaded = await store.ReadAsync(notePath, CancellationToken.None);
|
|
||||||
|
|
||||||
Assert.Equal(["Ada"], loaded.Frontmatter.Attendees);
|
|
||||||
Assert.Equal(["Meeting Assistant"], loaded.Frontmatter.Projects);
|
|
||||||
Assert.Equal("User notes.", loaded.UserNotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ActionLinkEscapesSummaryFileName()
|
public void ActionLinkEscapesSummaryFileName()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,34 +63,6 @@ public sealed class ProjectKnowledgeToolTests
|
|||||||
await tools.Search("alpha"));
|
await tools.Search("alpha"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ListProjectsAcceptsScalarProjectFrontmatter()
|
|
||||||
{
|
|
||||||
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"));
|
|
||||||
var projectsRoot = Path.Combine(root, "Projects");
|
|
||||||
Directory.CreateDirectory(Path.Combine(projectsRoot, "MeetingAssistant"));
|
|
||||||
var artifacts = CreateArtifacts(root);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(artifacts.MeetingNotePath)!);
|
|
||||||
await File.WriteAllTextAsync(
|
|
||||||
artifacts.MeetingNotePath,
|
|
||||||
"""
|
|
||||||
---
|
|
||||||
projects: MeetingAssistant
|
|
||||||
---
|
|
||||||
""");
|
|
||||||
var tools = new MeetingSummaryTools(
|
|
||||||
artifacts,
|
|
||||||
new MeetingAssistantOptions
|
|
||||||
{
|
|
||||||
Vault =
|
|
||||||
{
|
|
||||||
ProjectsFolder = projectsRoot
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.Equal("MeetingAssistant", await tools.ListProjects());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WriteProjectFileSupportsAppendReplaceInsertOverwriteAndCreate()
|
public async Task WriteProjectFileSupportsAppendReplaceInsertOverwriteAndCreate()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace MeetingAssistant.Tests;
|
|||||||
|
|
||||||
public sealed class RecordingCoordinatorTests
|
public sealed class RecordingCoordinatorTests
|
||||||
{
|
{
|
||||||
private static async Task WaitUntilAsync(Func<bool> condition, string? timeoutMessage = null)
|
private static async Task WaitUntilAsync(Func<bool> condition)
|
||||||
{
|
{
|
||||||
var deadline = DateTimeOffset.UtcNow.AddSeconds(15);
|
var deadline = DateTimeOffset.UtcNow.AddSeconds(15);
|
||||||
while (DateTimeOffset.UtcNow < deadline)
|
while (DateTimeOffset.UtcNow < deadline)
|
||||||
@@ -28,7 +28,7 @@ public sealed class RecordingCoordinatorTests
|
|||||||
await Task.Delay(25);
|
await Task.Delay(25);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TimeoutException(timeoutMessage ?? "Condition was not met.");
|
throw new TimeoutException("Condition was not met.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task AppendTextWithRetryAsync(string path, string text)
|
private static async Task AppendTextWithRetryAsync(string path, string text)
|
||||||
@@ -365,7 +365,6 @@ public sealed class RecordingCoordinatorTests
|
|||||||
await WaitUntilAsync(() => transcriptStore.Segments.Any(segment =>
|
await WaitUntilAsync(() => transcriptStore.Segments.Any(segment =>
|
||||||
segment.Text.Contains("latest transcript text", StringComparison.Ordinal)));
|
segment.Text.Contains("latest transcript text", StringComparison.Ordinal)));
|
||||||
|
|
||||||
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
|
||||||
clock.Advance(TimeSpan.FromSeconds(2));
|
clock.Advance(TimeSpan.FromSeconds(2));
|
||||||
await promptService.WaitForPromptAsync();
|
await promptService.WaitForPromptAsync();
|
||||||
await WaitUntilAsync(() => summaryPipeline.WasRun);
|
await WaitUntilAsync(() => summaryPipeline.WasRun);
|
||||||
@@ -415,10 +414,8 @@ public sealed class RecordingCoordinatorTests
|
|||||||
|
|
||||||
await coordinator.StartAsync(CancellationToken.None);
|
await coordinator.StartAsync(CancellationToken.None);
|
||||||
|
|
||||||
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
|
||||||
clock.Advance(TimeSpan.FromSeconds(5));
|
clock.Advance(TimeSpan.FromSeconds(5));
|
||||||
await WaitUntilAsync(() => !coordinator.CurrentStatus.IsRecording);
|
await WaitUntilAsync(() => !coordinator.CurrentStatus.IsRecording);
|
||||||
await WaitUntilAsync(() => summaryPipeline.WasRun);
|
|
||||||
|
|
||||||
Assert.Empty(promptService.Requests);
|
Assert.Empty(promptService.Requests);
|
||||||
Assert.False(coordinator.CurrentStatus.IsRecording);
|
Assert.False(coordinator.CurrentStatus.IsRecording);
|
||||||
@@ -462,7 +459,6 @@ public sealed class RecordingCoordinatorTests
|
|||||||
inactivityClock: clock);
|
inactivityClock: clock);
|
||||||
|
|
||||||
await coordinator.StartAsync(CancellationToken.None);
|
await coordinator.StartAsync(CancellationToken.None);
|
||||||
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
|
||||||
clock.Advance(TimeSpan.FromSeconds(2));
|
clock.Advance(TimeSpan.FromSeconds(2));
|
||||||
await promptService.WaitForPromptAsync();
|
await promptService.WaitForPromptAsync();
|
||||||
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
||||||
@@ -513,14 +509,12 @@ public sealed class RecordingCoordinatorTests
|
|||||||
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
|
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
|
||||||
await WaitUntilAsync(() => transcriptStore.Segments.Any(segment =>
|
await WaitUntilAsync(() => transcriptStore.Segments.Any(segment =>
|
||||||
segment.Text.Contains("chunk:2", StringComparison.Ordinal)));
|
segment.Text.Contains("chunk:2", StringComparison.Ordinal)));
|
||||||
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
|
||||||
clock.Advance(TimeSpan.FromSeconds(2));
|
clock.Advance(TimeSpan.FromSeconds(2));
|
||||||
await WaitUntilAsync(() => promptService.Requests.Count == 1);
|
await WaitUntilAsync(() => promptService.Requests.Count == 1);
|
||||||
|
|
||||||
await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0], 16000, 1), CancellationToken.None);
|
await audioSource.WriteAsync(new AudioChunk([1, 0, 2, 0], 16000, 1), CancellationToken.None);
|
||||||
await WaitUntilAsync(() => transcriptStore.Segments.Any(segment =>
|
await WaitUntilAsync(() => transcriptStore.Segments.Any(segment =>
|
||||||
segment.Text.Contains("chunk:4", StringComparison.Ordinal)));
|
segment.Text.Contains("chunk:4", StringComparison.Ordinal)));
|
||||||
await WaitUntilAsync(() => clock.PendingDelayCount > 0);
|
|
||||||
clock.Advance(TimeSpan.FromSeconds(2));
|
clock.Advance(TimeSpan.FromSeconds(2));
|
||||||
await WaitUntilAsync(() => promptService.Requests.Count == 2);
|
await WaitUntilAsync(() => promptService.Requests.Count == 2);
|
||||||
|
|
||||||
@@ -843,7 +837,6 @@ public sealed class RecordingCoordinatorTests
|
|||||||
};
|
};
|
||||||
var audioSource = new ControlledAudioSource();
|
var audioSource = new ControlledAudioSource();
|
||||||
var summaryPipeline = new CapturingMeetingSummaryPipeline();
|
var summaryPipeline = new CapturingMeetingSummaryPipeline();
|
||||||
var metadataProvider = new BlockingMeetingMetadataProvider(new MeetingMetadata("", [], ""));
|
|
||||||
var coordinator = new MeetingRecordingCoordinator(
|
var coordinator = new MeetingRecordingCoordinator(
|
||||||
audioSource,
|
audioSource,
|
||||||
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
new TestSpeechRecognitionPipelineFactory(new EchoStreamingTranscriptionProvider()),
|
||||||
@@ -854,44 +847,37 @@ public sealed class RecordingCoordinatorTests
|
|||||||
new InMemoryRecordedAudioStore(),
|
new InMemoryRecordedAudioStore(),
|
||||||
summaryPipeline,
|
summaryPipeline,
|
||||||
Options.Create(options),
|
Options.Create(options),
|
||||||
NullLogger<MeetingRecordingCoordinator>.Instance,
|
NullLogger<MeetingRecordingCoordinator>.Instance);
|
||||||
meetingMetadataProvider: metadataProvider);
|
|
||||||
|
|
||||||
try
|
var started = await coordinator.StartAsync(CancellationToken.None);
|
||||||
{
|
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
|
||||||
var started = await coordinator.StartAsync(CancellationToken.None);
|
await WaitUntilAsync(() =>
|
||||||
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
|
FileContainsText(started.AssistantContextPath!, "state: transcribing"));
|
||||||
await WaitUntilAsync(() => File.Exists(started.AssistantContextPath!));
|
var attachmentPath = Path.Combine(
|
||||||
var attachmentPath = Path.Combine(
|
Path.GetDirectoryName(started.AssistantContextPath!)!,
|
||||||
Path.GetDirectoryName(started.AssistantContextPath!)!,
|
"Attachments",
|
||||||
"Attachments",
|
"20260527-093135-123-000010-cropped.png");
|
||||||
"20260527-093135-123-000010-cropped.png");
|
Directory.CreateDirectory(Path.GetDirectoryName(attachmentPath)!);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(attachmentPath)!);
|
await File.WriteAllTextAsync(attachmentPath, "image");
|
||||||
await File.WriteAllTextAsync(attachmentPath, "image");
|
await AppendTextWithRetryAsync(
|
||||||
await AppendTextWithRetryAsync(
|
started.AssistantContextPath!,
|
||||||
started.AssistantContextPath!,
|
Environment.NewLine + "" + Environment.NewLine);
|
||||||
Environment.NewLine + "" + Environment.NewLine);
|
Directory.CreateDirectory(Path.GetDirectoryName(started.SummaryPath!)!);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(started.SummaryPath!)!);
|
await File.WriteAllTextAsync(started.SummaryPath!, "draft summary");
|
||||||
await File.WriteAllTextAsync(started.SummaryPath!, "draft summary");
|
|
||||||
|
|
||||||
var aborted = await coordinator.AbortAsync(CancellationToken.None);
|
var aborted = await coordinator.AbortAsync(CancellationToken.None);
|
||||||
|
|
||||||
Assert.False(aborted.IsRecording);
|
Assert.False(aborted.IsRecording);
|
||||||
Assert.Null(aborted.MeetingNotePath);
|
Assert.Null(aborted.MeetingNotePath);
|
||||||
Assert.False(File.Exists(started.MeetingNotePath));
|
Assert.False(File.Exists(started.MeetingNotePath));
|
||||||
Assert.False(File.Exists(started.TranscriptPath));
|
Assert.False(File.Exists(started.TranscriptPath));
|
||||||
Assert.False(File.Exists(started.AssistantContextPath));
|
Assert.False(File.Exists(started.AssistantContextPath));
|
||||||
Assert.False(File.Exists(started.SummaryPath));
|
Assert.False(File.Exists(started.SummaryPath));
|
||||||
Assert.False(File.Exists(attachmentPath));
|
Assert.False(File.Exists(attachmentPath));
|
||||||
Assert.Null(summaryPipeline.Artifacts);
|
Assert.Null(summaryPipeline.Artifacts);
|
||||||
|
|
||||||
await Task.Delay(250);
|
await Task.Delay(250);
|
||||||
Assert.False(File.Exists(started.AssistantContextPath));
|
Assert.False(File.Exists(started.AssistantContextPath));
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
metadataProvider.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -2181,8 +2167,8 @@ public sealed class RecordingCoordinatorTests
|
|||||||
|
|
||||||
private sealed class InMemoryTranscriptStore : ITranscriptStore
|
private sealed class InMemoryTranscriptStore : ITranscriptStore
|
||||||
{
|
{
|
||||||
private readonly object gate = new();
|
|
||||||
private readonly List<TranscriptionSegment> segments = [];
|
private readonly List<TranscriptionSegment> segments = [];
|
||||||
|
private readonly TaskCompletionSource segmentWritten = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private readonly string transcriptPath;
|
private readonly string transcriptPath;
|
||||||
|
|
||||||
public InMemoryTranscriptStore(string transcriptPath = "memory-transcript.md")
|
public InMemoryTranscriptStore(string transcriptPath = "memory-transcript.md")
|
||||||
@@ -2205,33 +2191,30 @@ public sealed class RecordingCoordinatorTests
|
|||||||
|
|
||||||
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
public Task AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
lock (gate)
|
segments.Add(ParseTranscriptLine(line));
|
||||||
{
|
segmentWritten.TrySetResult();
|
||||||
segments.Add(ParseTranscriptLine(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WaitForTextAsync(string text)
|
public async Task WaitForTextAsync(string text)
|
||||||
{
|
{
|
||||||
return WaitUntilAsync(
|
var deadline = DateTimeOffset.UtcNow.AddSeconds(5);
|
||||||
() => Segments.Any(segment => segment?.Text?.Contains(text, StringComparison.Ordinal) == true),
|
while (DateTimeOffset.UtcNow < deadline)
|
||||||
$"Segment containing '{text}' was not written.");
|
{
|
||||||
|
if (segments.Any(segment => segment?.Text?.Contains(text, StringComparison.Ordinal) == true))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await segmentWritten.Task.WaitAsync(TimeSpan.FromMilliseconds(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException($"Segment containing '{text}' was not written.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<TranscriptionSegment> ReplacedSegments { get; private set; } = [];
|
public IReadOnlyList<TranscriptionSegment> ReplacedSegments { get; private set; } = [];
|
||||||
|
|
||||||
public IReadOnlyList<TranscriptionSegment> Segments
|
public IReadOnlyList<TranscriptionSegment> Segments => segments;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (gate)
|
|
||||||
{
|
|
||||||
return segments.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public MeetingNote? MetadataMeetingNote { get; private set; }
|
public MeetingNote? MetadataMeetingNote { get; private set; }
|
||||||
|
|
||||||
@@ -2259,6 +2242,7 @@ public sealed class RecordingCoordinatorTests
|
|||||||
{
|
{
|
||||||
private int created;
|
private int created;
|
||||||
private int appendCount;
|
private int appendCount;
|
||||||
|
private TaskCompletionSource appendObserved = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
public List<TranscriptWrite> AppendHistory { get; } = [];
|
public List<TranscriptWrite> AppendHistory { get; } = [];
|
||||||
|
|
||||||
@@ -2284,14 +2268,29 @@ public sealed class RecordingCoordinatorTests
|
|||||||
{
|
{
|
||||||
AppendHistory.Add(new TranscriptWrite(session, ParseTranscriptLine(line)));
|
AppendHistory.Add(new TranscriptWrite(session, ParseTranscriptLine(line)));
|
||||||
Interlocked.Increment(ref appendCount);
|
Interlocked.Increment(ref appendCount);
|
||||||
|
appendObserved.TrySetResult();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task WaitForAppendCountAsync(int expectedCount)
|
public async Task WaitForAppendCountAsync(int expectedCount)
|
||||||
{
|
{
|
||||||
return WaitUntilAsync(
|
var deadline = DateTimeOffset.UtcNow.AddSeconds(5);
|
||||||
() => Volatile.Read(ref appendCount) >= expectedCount,
|
while (DateTimeOffset.UtcNow < deadline)
|
||||||
$"Expected {expectedCount} transcript append(s).");
|
{
|
||||||
|
if (Volatile.Read(ref appendCount) >= expectedCount)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var observed = appendObserved;
|
||||||
|
await observed.Task.WaitAsync(TimeSpan.FromMilliseconds(100));
|
||||||
|
if (ReferenceEquals(observed, appendObserved))
|
||||||
|
{
|
||||||
|
appendObserved = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException($"Expected {expectedCount} transcript append(s).");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ReplaceLinesAsync(
|
public Task ReplaceLinesAsync(
|
||||||
|
|||||||
@@ -20,16 +20,16 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.10.0" />
|
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.2" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.9.0" />
|
<PackageReference Include="DiffPlex" Version="1.9.0" />
|
||||||
<PackageReference Include="Microsoft.CognitiveServices.Speech" Version="$(MicrosoftSpeechVersion)" />
|
<PackageReference Include="Microsoft.CognitiveServices.Speech" Version="$(MicrosoftSpeechVersion)" />
|
||||||
<PackageReference Include="Microsoft.CognitiveServices.Speech.Extension.MAS" Version="$(MicrosoftSpeechVersion)" ExcludeAssets="build" />
|
<PackageReference Include="Microsoft.CognitiveServices.Speech.Extension.MAS" Version="$(MicrosoftSpeechVersion)" ExcludeAssets="build" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
|
||||||
<PackageReference Include="NAudio" Version="2.3.0" />
|
<PackageReference Include="NAudio" Version="2.3.0" />
|
||||||
<PackageReference Include="NCalcSync" Version="5.12.0" />
|
<PackageReference Include="NCalcSync" Version="5.12.0" />
|
||||||
<PackageReference Include="RazorLight" Version="2.3.1" />
|
<PackageReference Include="RazorLight" Version="2.3.1" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.9" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.9" />
|
||||||
<PackageReference Include="Whisper.net" Version="1.9.1" />
|
<PackageReference Include="Whisper.net" Version="1.9.0" />
|
||||||
<PackageReference Include="Whisper.net.Runtime" Version="1.9.0" />
|
<PackageReference Include="Whisper.net.Runtime" Version="1.9.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="18.0.0" />
|
<PackageReference Include="YamlDotNet" Version="18.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
using YamlDotNet.Serialization;
|
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
|
||||||
|
|
||||||
namespace MeetingAssistant.MeetingNotes;
|
|
||||||
|
|
||||||
internal static class FrontmatterYamlDeserializer
|
|
||||||
{
|
|
||||||
public static IDeserializer Create()
|
|
||||||
{
|
|
||||||
return new DeserializerBuilder()
|
|
||||||
.WithTypeConverter(new ScalarStringListYamlTypeConverter())
|
|
||||||
.IgnoreUnmatchedProperties()
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IDeserializer CreateWithUnderscoredNaming()
|
|
||||||
{
|
|
||||||
return new DeserializerBuilder()
|
|
||||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
|
||||||
.WithTypeConverter(new ScalarStringListYamlTypeConverter())
|
|
||||||
.IgnoreUnmatchedProperties()
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,9 @@ public sealed class MarkdownMeetingNoteStore : IMeetingNoteStore
|
|||||||
{
|
{
|
||||||
this.options = options.Value;
|
this.options = options.Value;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
yamlDeserializer = FrontmatterYamlDeserializer.Create();
|
yamlDeserializer = new DeserializerBuilder()
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MeetingNote> SaveAsync(MeetingNote note, CancellationToken cancellationToken)
|
public async Task<MeetingNote> SaveAsync(MeetingNote note, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
using YamlDotNet.Core;
|
|
||||||
using YamlDotNet.Core.Events;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace MeetingAssistant.MeetingNotes;
|
|
||||||
|
|
||||||
internal sealed class ScalarStringListYamlTypeConverter : IYamlTypeConverter
|
|
||||||
{
|
|
||||||
public bool Accepts(Type type)
|
|
||||||
{
|
|
||||||
return type == typeof(List<string>);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer)
|
|
||||||
{
|
|
||||||
if (parser.TryConsume<Scalar>(out var scalar))
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(scalar.Value)
|
|
||||||
? []
|
|
||||||
: new List<string> { scalar.Value };
|
|
||||||
}
|
|
||||||
|
|
||||||
var values = new List<string>();
|
|
||||||
parser.Consume<SequenceStart>();
|
|
||||||
while (!parser.TryConsume<SequenceEnd>(out _))
|
|
||||||
{
|
|
||||||
var item = parser.Consume<Scalar>();
|
|
||||||
values.Add(item.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
|
|
||||||
{
|
|
||||||
serializer(value, typeof(IEnumerable<string>));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,9 @@ public sealed record BoundMeetingProject(string Name, string Path);
|
|||||||
|
|
||||||
public sealed class BoundMeetingProjectResolver
|
public sealed class BoundMeetingProjectResolver
|
||||||
{
|
{
|
||||||
private static readonly IDeserializer YamlDeserializer = FrontmatterYamlDeserializer.Create();
|
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
|
||||||
private readonly MeetingAssistantOptions options;
|
private readonly MeetingAssistantOptions options;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ namespace MeetingAssistant.Summary;
|
|||||||
|
|
||||||
internal static class MeetingSummaryFrontmatterFactory
|
internal static class MeetingSummaryFrontmatterFactory
|
||||||
{
|
{
|
||||||
private static readonly IDeserializer YamlDeserializer = FrontmatterYamlDeserializer.Create();
|
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
|
||||||
public static async Task<MeetingArtifactFrontmatter> CreateAsync(
|
public static async Task<MeetingArtifactFrontmatter> CreateAsync(
|
||||||
MeetingSessionArtifacts artifacts,
|
MeetingSessionArtifacts artifacts,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ namespace MeetingAssistant.Summary;
|
|||||||
|
|
||||||
public sealed class MeetingSummaryTools
|
public sealed class MeetingSummaryTools
|
||||||
{
|
{
|
||||||
private static readonly IDeserializer YamlDeserializer = FrontmatterYamlDeserializer.Create();
|
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
|
||||||
private readonly MeetingSessionArtifacts artifacts;
|
private readonly MeetingSessionArtifacts artifacts;
|
||||||
private readonly MeetingAssistantOptions options;
|
private readonly MeetingAssistantOptions options;
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using YamlDotNet.Core;
|
using YamlDotNet.Core;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
namespace MeetingAssistant.Workflow;
|
namespace MeetingAssistant.Workflow;
|
||||||
|
|
||||||
public sealed class WorkflowRulesEditorTools
|
public sealed class WorkflowRulesEditorTools
|
||||||
{
|
{
|
||||||
private static readonly IDeserializer YamlDeserializer = FrontmatterYamlDeserializer.CreateWithUnderscoredNaming();
|
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
|
||||||
|
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
|
|||||||
@@ -48,12 +48,6 @@ Generated artifact notes SHALL link only to the other notes from the same run an
|
|||||||
- **WHEN** Meeting Assistant creates a meeting note
|
- **WHEN** Meeting Assistant creates a meeting note
|
||||||
- **THEN** the note frontmatter links to the configured transcript, assistant context, and summary note locations
|
- **THEN** the note frontmatter links to the configured transcript, assistant context, and summary note locations
|
||||||
|
|
||||||
#### Scenario: Scalar list frontmatter is normalized
|
|
||||||
- **GIVEN** a meeting note frontmatter field that Meeting Assistant expects as a list is stored as a scalar string
|
|
||||||
- **WHEN** Meeting Assistant reads the meeting note
|
|
||||||
- **THEN** it treats the scalar string as a one-item list
|
|
||||||
- **AND** continues processing the meeting note
|
|
||||||
|
|
||||||
#### Scenario: Generated artifacts do not self-reference
|
#### Scenario: Generated artifacts do not self-reference
|
||||||
- **WHEN** Meeting Assistant writes transcript, assistant context, or summary artifact frontmatter
|
- **WHEN** Meeting Assistant writes transcript, assistant context, or summary artifact frontmatter
|
||||||
- **THEN** the artifact frontmatter links to the other run notes
|
- **THEN** the artifact frontmatter links to the other run notes
|
||||||
|
|||||||
Reference in New Issue
Block a user