From 68ade5afc89dcf8b08760c78e926a7ba47e39e3d Mon Sep 17 00:00:00 2001
From: VasiliyF <5789590+vfofanov@users.noreply.github.com>
Date: Sat, 3 Oct 2026 20:43:44 +0200
Subject: [PATCH 1/4] Add the json-observer-masking skill and a test project
that runs every skill snippet
Every csharp block under skills/ is compiled against the libraries and run: programs must print their // Output: comment, test classes run their [Fact]/[Theory] methods.
---
...tem.Text.Json.Observer.Skills.Tests.csproj | 13 +
.../SkillDocuments.cs | 22 ++
.../SkillSnippetTests.cs | 249 +++++++++++++++
.../SkillStructureTests.cs | 70 ++++
.../xunit.runner.json | 3 +
DragoAnt.System.Text.Json.slnx | 1 +
skills/json-observer-masking/SKILL.md | 65 ++++
skills/json-observer-masking/examples.md | 301 ++++++++++++++++++
.../migrating-from-1x.md | 106 ++++++
skills/json-observer-masking/pitfalls.md | 144 +++++++++
skills/json-observer-masking/recipes.md | 202 ++++++++++++
11 files changed, 1176 insertions(+)
create mode 100644 DragoAnt.System.Text.Json.Observer.Skills.Tests/DragoAnt.System.Text.Json.Observer.Skills.Tests.csproj
create mode 100644 DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillDocuments.cs
create mode 100644 DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillSnippetTests.cs
create mode 100644 DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillStructureTests.cs
create mode 100644 DragoAnt.System.Text.Json.Observer.Skills.Tests/xunit.runner.json
create mode 100644 skills/json-observer-masking/SKILL.md
create mode 100644 skills/json-observer-masking/examples.md
create mode 100644 skills/json-observer-masking/migrating-from-1x.md
create mode 100644 skills/json-observer-masking/pitfalls.md
create mode 100644 skills/json-observer-masking/recipes.md
diff --git a/DragoAnt.System.Text.Json.Observer.Skills.Tests/DragoAnt.System.Text.Json.Observer.Skills.Tests.csproj b/DragoAnt.System.Text.Json.Observer.Skills.Tests/DragoAnt.System.Text.Json.Observer.Skills.Tests.csproj
new file mode 100644
index 0000000..4cd24c7
--- /dev/null
+++ b/DragoAnt.System.Text.Json.Observer.Skills.Tests/DragoAnt.System.Text.Json.Observer.Skills.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillDocuments.cs b/DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillDocuments.cs
new file mode 100644
index 0000000..b2341f9
--- /dev/null
+++ b/DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillDocuments.cs
@@ -0,0 +1,22 @@
+namespace DragoAnt.System.Text.Json.Observer.Skills.Tests;
+
+internal static class SkillDocuments
+{
+ public static string Root { get; } = Path.Combine(AppContext.BaseDirectory, "skills");
+
+ /// Every markdown file under skills/, as a path relative to it with forward slashes.
+ public static IReadOnlyList All { get; } = Directory.Exists(Root)
+ ? Directory.GetFiles(Root, "*.md", SearchOption.AllDirectories)
+ .Select(f => Path.GetRelativePath(Root, f).Replace('\\', '/'))
+ .Order(StringComparer.Ordinal)
+ .ToArray()
+ : [];
+
+ /// Skill folder names: the directories holding a SKILL.md.
+ public static IReadOnlyList Skills { get; } = All
+ .Where(d => d.EndsWith("/SKILL.md", StringComparison.Ordinal))
+ .Select(d => d[..d.IndexOf('/')])
+ .ToArray();
+
+ public static string[] ReadLines(string document) => File.ReadAllLines(Path.Combine(Root, document));
+}
diff --git a/DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillSnippetTests.cs b/DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillSnippetTests.cs
new file mode 100644
index 0000000..8be4646
--- /dev/null
+++ b/DragoAnt.System.Text.Json.Observer.Skills.Tests/SkillSnippetTests.cs
@@ -0,0 +1,249 @@
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace DragoAnt.System.Text.Json.Observer.Skills.Tests;
+
+///
+/// Every ```csharp block under skills/ is compiled against the current libraries and run.
+/// A block with [Fact] or [Theory] is an xUnit test class: each test method runs and must pass.
+/// Any other block is a program: it must run without throwing and print what its // Output: comment says.
+/// A block preceded by <!-- doc-test: skip --> is a fragment and is not compiled.
+///
+public sealed partial class SkillSnippetTests
+{
+ private static readonly Lazy References = new(LoadReferences);
+
+ public static TheoryData Snippets()
+ {
+ var data = new TheoryData();
+ foreach (var document in SkillDocuments.All)
+ {
+ var count = Extract(document).Count;
+ for (var i = 0; i < count; i++)
+ {
+ data.Add(document, i);
+ }
+ }
+
+ return data;
+ }
+
+ [Fact]
+ public void EverySkill_HasRunnableSnippets()
+ {
+ foreach (var skill in SkillDocuments.Skills)
+ {
+ SkillDocuments.All.Where(d => d.StartsWith(skill + "/", StringComparison.Ordinal))
+ .Sum(d => Extract(d).Count)
+ .Should().BeGreaterThan(0, skill);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(Snippets))]
+ public async Task Snippet_CompilesAndRuns(string document, int index)
+ {
+ var snippet = Extract(document)[index];
+ var where = $"{document} snippet at line {snippet.Line}";
+
+ var assembly = Compile(snippet, $"{document.Replace('/', '_').Replace('.', '_').Replace('-', '_')}_{index}", out var diagnostics);
+ assembly.Should().NotBeNull($"{where} must compile:{Environment.NewLine}{diagnostics}");
+
+ if (snippet.IsTestClass)
+ {
+ var ran = await RunTests(assembly!, where);
+ ran.Should().BeGreaterThan(0, $"{where} declares at least one test");
+ return;
+ }
+
+ var printed = await RunProgram(assembly!);
+ if (snippet.ExpectedOutput is { } expected)
+ {
+ printed.TrimEnd().Should().Be(expected, $"{where} prints its // Output: comment");
+ }
+ }
+
+ private sealed record Snippet(int Line, string Code, string? ExpectedOutput, bool IsTestClass);
+
+ private static List Extract(string document)
+ {
+ var lines = SkillDocuments.ReadLines(document);
+ var snippets = new List();
+ for (var i = 0; i < lines.Length; i++)
+ {
+ if (lines[i].Trim() != "```csharp")
+ {
+ continue;
+ }
+
+ var skip = i > 0 && lines[i - 1].Contains("doc-test: skip", StringComparison.Ordinal);
+ var start = i + 1;
+ var end = start;
+ while (end < lines.Length && lines[end].Trim() != "```")
+ {
+ end++;
+ }
+
+ if (!skip)
+ {
+ var code = lines[start..end];
+ var isTestClass = code.Any(l => TestAttribute().IsMatch(l));
+ snippets.Add(new Snippet(start + 1, string.Join('\n', code), isTestClass ? null : ExpectedOutput(code), isTestClass));
+ }
+
+ i = end;
+ }
+
+ return snippets;
+ }
+
+ private static string? ExpectedOutput(string[] code)
+ {
+ var at = Array.FindIndex(code, l => l.Trim() == "// Output:");
+ if (at < 0)
+ {
+ return null;
+ }
+
+ var output = code.Skip(at + 1).TakeWhile(l => l.TrimStart().StartsWith("//", StringComparison.Ordinal))
+ .Select(l => l.TrimStart()[2..].TrimStart());
+ return string.Join('\n', output);
+ }
+
+ private static Assembly? Compile(Snippet snippet, string name, out string diagnostics)
+ {
+ var tree = CSharpSyntaxTree.ParseText(ImplicitUsings + snippet.Code, new CSharpParseOptions(LanguageVersion.Latest));
+ var compilation = CSharpCompilation.Create(
+ name,
+ [tree],
+ References.Value,
+ new CSharpCompilationOptions(
+ snippet.IsTestClass ? OutputKind.DynamicallyLinkedLibrary : OutputKind.ConsoleApplication,
+ nullableContextOptions: NullableContextOptions.Enable));
+
+ using var stream = new MemoryStream();
+ var result = compilation.Emit(stream);
+ diagnostics = string.Join(Environment.NewLine, result.Diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning));
+ return result.Success ? Assembly.Load(stream.ToArray()) : null;
+ }
+
+ private static async Task RunProgram(Assembly assembly)
+ {
+ var entry = assembly.EntryPoint!;
+ var original = Console.Out;
+ var printed = new StringWriter();
+ Console.SetOut(printed);
+ try
+ {
+ await Await(Invoke(entry, null, entry.GetParameters().Length == 0 ? null : [Array.Empty()]));
+ }
+ finally
+ {
+ Console.SetOut(original);
+ }
+
+ return NewLines().Replace(printed.ToString(), "\n");
+ }
+
+ private static async Task RunTests(Assembly assembly, string where)
+ {
+ var ran = 0;
+ foreach (var type in assembly.GetTypes().Where(t => t is { IsClass: true, IsAbstract: false, IsPublic: true }))
+ {
+ foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
+ {
+ var attributes = method.GetCustomAttributes().ToList();
+ var test = attributes.FirstOrDefault(a => a.GetType().Name is "FactAttribute" or "TheoryAttribute");
+ if (test is null || test.GetType().GetProperty("Skip")?.GetValue(test) is string)
+ {
+ continue;
+ }
+
+ List