Files
Dan Moseley 042a9d2b2c Reorganize plugins into domain-specific groupings (#274)
* Reorganize plugins per agreed structure (#271)

Split the dotnet plugin into domain-specific plugins per discussion
in #271:

- dotnet: narrowed to common everyday C#/.NET coding tasks
- dotnet-diag: perf investigations, debugging, incident analysis
- dotnet-data: data access and Entity Framework
- dotnet-aspnet, dotnet-wpf, dotnet-winforms, dotnet-maui: scaffolded
  empty to convey intent for future framework-specific skills

Moved skills, tests, and CODEOWNERS entries accordingly.
No file content was changed; all skill/test moves are pure renames.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improve dotnet-diag and dotnet-winforms descriptions

- dotnet-diag: add '.NET' qualifier to clarify scope
- dotnet-winforms: use official 'Windows Forms (WinForms)' name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add .NET qualifier to dotnet-data description

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove empty plugins; list them in CONTRIBUTING.md as future options

Remove the four empty placeholder plugins (dotnet-aspnet, dotnet-wpf,
dotnet-winforms, dotnet-maui) that had only plugin.json and no skills.

Add a 'Plugin organization' section to CONTRIBUTING.md that describes the
domain-specific plugin groupings introduced in this PR and lists the four
removed plugin names as reserved candidates for future contributions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Create dotnet-upgrade plugin with migration/upgrade skills

Move migrate-nullable-references, thread-abort-migration, and
dotnet-aot-compat from dotnet to the new dotnet-upgrade plugin.

Update CODEOWNERS paths (same owners), CONTRIBUTING.md plugin table,
and fix the thread-abort-migration SKILL.md link in CONTRIBUTING.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 21:01:52 -08:00

588 lines
24 KiB
YAML
Raw Permalink Blame History

scenarios:
- name: "Enable NRT in a small library with mixed nullability"
prompt: |
Enable nullable reference types in this C# project and resolve all warnings.
The project is at TaskManager/TaskManager.csproj. Make sure the public API
surface accurately reflects nullability.
setup:
files:
- path: "TaskManager/TaskManager.csproj"
content: |
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
</Project>
- path: "TaskManager/Models/TaskItem.cs"
content: |
namespace TaskManager.Models;
public class TaskItem
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string AssignedTo { get; set; }
public DateTime? DueDate { get; set; }
public bool IsCompleted { get; set; }
public List<string> Tags { get; set; } = new();
public TaskItem(string title)
{
Title = title;
}
}
- path: "TaskManager/Models/Project.cs"
content: |
namespace TaskManager.Models;
public class Project
{
public string Name { get; set; }
public string Owner { get; set; }
public List<TaskItem> Tasks { get; } = new();
public Project(string name, string owner)
{
Name = name;
Owner = owner;
}
public TaskItem FindTaskByTitle(string title)
{
return Tasks.FirstOrDefault(t => t.Title == title);
}
}
- path: "TaskManager/Services/ITaskRepository.cs"
content: |
using TaskManager.Models;
namespace TaskManager.Services;
public interface ITaskRepository
{
TaskItem GetById(int id);
IReadOnlyList<TaskItem> GetAll();
void Add(TaskItem task);
void Remove(int id);
TaskItem FindByTitle(string title);
}
- path: "TaskManager/Services/InMemoryTaskRepository.cs"
content: |
using TaskManager.Models;
namespace TaskManager.Services;
public class InMemoryTaskRepository : ITaskRepository
{
private readonly List<TaskItem> _tasks = new();
private int _nextId = 1;
public TaskItem GetById(int id)
{
return _tasks.FirstOrDefault(t => t.Id == id);
}
public IReadOnlyList<TaskItem> GetAll() => _tasks.AsReadOnly();
public void Add(TaskItem task)
{
task.Id = _nextId++;
_tasks.Add(task);
}
public void Remove(int id)
{
var task = GetById(id);
if (task != null)
_tasks.Remove(task);
}
public TaskItem FindByTitle(string title)
{
return _tasks.FirstOrDefault(t =>
string.Equals(t.Title, title, StringComparison.OrdinalIgnoreCase));
}
}
- path: "TaskManager/Services/TaskService.cs"
content: |
using TaskManager.Models;
namespace TaskManager.Services;
public class TaskService
{
private readonly ITaskRepository _repository;
private string _defaultAssignee;
public TaskService(ITaskRepository repository, string defaultAssignee)
{
_repository = repository;
_defaultAssignee = defaultAssignee;
}
public TaskItem CreateTask(string title, string description, string assignedTo = null)
{
var task = new TaskItem(title)
{
Description = description,
AssignedTo = assignedTo ?? _defaultAssignee
};
_repository.Add(task);
return task;
}
public TaskItem GetTask(int id) => _repository.GetById(id);
public bool CompleteTask(int id)
{
var task = _repository.GetById(id);
if (task == null) return false;
task.IsCompleted = true;
return true;
}
public IReadOnlyList<TaskItem> GetTasksByAssignee(string assignee)
{
return _repository.GetAll()
.Where(t => string.Equals(t.AssignedTo, assignee, StringComparison.OrdinalIgnoreCase))
.ToList();
}
}
- path: "TaskManager/Services/TaskFormatter.cs"
content: |
using TaskManager.Models;
namespace TaskManager.Services;
public static class TaskFormatter
{
public static string FormatTask(TaskItem task)
{
var status = task.IsCompleted ? "Done" : "Pending";
var assignee = task.AssignedTo ?? "Unassigned";
var due = task.DueDate.HasValue
? task.DueDate.Value.ToShortDateString()
: "No due date";
return $"[{status}] {task.Title} - {assignee} (Due: {due})";
}
public static string FormatProject(Project project)
{
var lines = new List<string>
{
$"Project: {project.Name} (Owner: {project.Owner})",
$"Tasks: {project.Tasks.Count}"
};
foreach (var task in project.Tasks)
{
lines.Add($" - {FormatTask(task)}");
}
return string.Join(Environment.NewLine, lines);
}
}
- path: "TaskManager/Services/CsvExporter.cs"
content: |
using TaskManager.Models;
namespace TaskManager.Services;
public class CsvExporter
{
private string _separator;
private string _header;
public CsvExporter(string separator = ",")
{
_separator = separator;
}
public string Export(IEnumerable<TaskItem> tasks)
{
_header = string.Join(_separator, "Id", "Title", "AssignedTo", "Completed");
var lines = new List<string> { _header };
foreach (var task in tasks)
{
var assignee = task.AssignedTo ?? "";
lines.Add(string.Join(_separator,
task.Id, task.Title, assignee, task.IsCompleted));
}
return string.Join(Environment.NewLine, lines);
}
}
assertions:
- type: "exit_success"
- type: "file_contains"
path: "TaskManager/TaskManager.csproj"
value: "<Nullable>enable</Nullable>"
- type: "file_contains"
path: "TaskManager/Services/ITaskRepository.cs"
value: "TaskItem?"
- type: "file_contains"
path: "TaskManager/Models/Project.cs"
value: "TaskItem?"
- type: "file_contains"
path: "TaskManager/Services/TaskService.cs"
value: "string?"
rubric:
- "Did the agent avoid adding `?` to value types? `int Id`, `bool IsCompleted`, `int id` parameters, and `int _nextId` must NOT get `?`. `DateTime? DueDate` already has `?` and must not become `DateTime??`. Check edits carefully for this mistake."
- "Did the agent correctly annotate methods that return null? `GetById`, `FindByTitle`, and `FindTaskByTitle` all use `FirstOrDefault` which returns null <20> their return types must be `TaskItem?`, not `TaskItem`. Both the interface `ITaskRepository` and implementation `InMemoryTaskRepository` must match."
- "Did the agent correctly handle the `_header` field in CsvExporter? It is set inside `Export()`, not the constructor <20> it should be `string? _header` or `string _header = null!`, not left as `string _header` which would produce CS8618."
- "Did the agent keep interface and implementation return types consistent? If `ITaskRepository.GetById` returns `TaskItem?`, then `InMemoryTaskRepository.GetById` must also return `TaskItem?`. Same for `FindByTitle`. Check both files."
- "Does the project build with zero CS86xx warnings after the agent's changes? The agent should have resolved all nullable warnings introduced by enabling NRT."
timeout: 300
- name: "File-by-file migration: only modify the targeted file"
prompt: |
This project has nullable reference types enabled project-wide, but each file
still has #nullable disable at the top. Remove #nullable disable from
UserProfile.cs ONLY and fix all resulting nullable warnings in that file.
Do NOT modify any other files.
setup:
files:
- path: "FileByFile/FileByFile.csproj"
content: |
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
- path: "FileByFile/UserProfile.cs"
content: |
#nullable disable
namespace FileByFile;
public class UserProfile
{
public string Name { get; set; }
public string Bio { get; set; }
public string Email { get; set; }
public UserProfile(string name, string email)
{
Name = name;
Email = email;
}
public string GetDisplayName()
{
return Bio != null ? $"{Name} - {Bio}" : Name;
}
}
- path: "FileByFile/OrderProcessor.cs"
content: |
#nullable disable
namespace FileByFile;
public class OrderProcessor
{
private string _connectionString;
public OrderProcessor(string connectionString)
{
_connectionString = connectionString;
}
public string ProcessOrder(string orderId)
{
return null;
}
}
- path: "FileByFile/NotificationService.cs"
content: |
#nullable disable
namespace FileByFile;
public class NotificationService
{
private string _endpoint;
public NotificationService(string endpoint)
{
_endpoint = endpoint;
}
public void Send(string message, string recipient)
{
}
public string GetLastError()
{
return null;
}
}
assertions:
- type: "exit_success"
- type: "file_not_contains"
path: "FileByFile/UserProfile.cs"
value: "#nullable disable"
- type: "file_contains"
path: "FileByFile/UserProfile.cs"
value: "string?"
- type: "file_contains"
path: "FileByFile/OrderProcessor.cs"
value: "#nullable disable"
- type: "file_contains"
path: "FileByFile/NotificationService.cs"
value: "#nullable disable"
rubric:
- "Did the agent follow the skill's file-by-file migration strategy, migrating UserProfile.cs completely (removing #nullable disable, annotating correctly) before moving on to other files?"
- "Did the agent correctly annotate Bio as string? (nullable) since it is never set in the constructor and can legitimately be null, while keeping Name and Email as non-nullable string since they are set via the constructor?"
- "Does the project build with zero errors after the agent's changes?"
timeout: 300
- name: "Enable NRT in ASP.NET Core Web API with EF Core"
prompt: |
This small project (6 files) needs `<Nullable>enable</Nullable>` added and all resulting
warnings fixed. It's a straightforward task. The project is at BookStore/BookStore.csproj.
It uses Entity Framework Core with an in-memory database and has a Web API controller.
Make sure annotations are correct for both EF Core entities and API models.
setup:
files:
- path: "BookStore/BookStore.csproj"
content: |
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
</ItemGroup>
</Project>
- path: "BookStore/Models/Author.cs"
content: |
namespace BookStore.Models;
public class Author
{
public int Id { get; set; }
public string Name { get; set; }
public string Biography { get; set; }
public List<Book> Books { get; set; } = new();
}
- path: "BookStore/Models/Book.cs"
content: |
namespace BookStore.Models;
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Isbn { get; set; }
public decimal Price { get; set; }
public string Summary { get; set; }
public int AuthorId { get; set; }
public Author Author { get; set; }
public int? CategoryId { get; set; }
public Category Category { get; set; }
}
- path: "BookStore/Models/Category.cs"
content: |
namespace BookStore.Models;
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public List<Book> Books { get; set; } = new();
}
- path: "BookStore/Data/BookStoreContext.cs"
content: |
using Microsoft.EntityFrameworkCore;
using BookStore.Models;
namespace BookStore.Data;
public class BookStoreContext : DbContext
{
public BookStoreContext(DbContextOptions<BookStoreContext> options) : base(options) { }
public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasOne(b => b.Category)
.WithMany(c => c.Books)
.HasForeignKey(b => b.CategoryId)
.IsRequired(false);
}
}
- path: "BookStore/Dtos/BookDto.cs"
content: |
namespace BookStore.Dtos;
public class CreateBookRequest
{
public string Title { get; set; }
public string Isbn { get; set; }
public decimal Price { get; set; }
public string Summary { get; set; }
public int AuthorId { get; set; }
public int? CategoryId { get; set; }
}
public class BookResponse
{
public int Id { get; set; }
public string Title { get; set; }
public string Isbn { get; set; }
public decimal Price { get; set; }
public string AuthorName { get; set; }
public string CategoryName { get; set; }
}
- path: "BookStore/Controllers/BooksController.cs"
content: |
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using BookStore.Data;
using BookStore.Dtos;
using BookStore.Models;
namespace BookStore.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly BookStoreContext _context;
public BooksController(BookStoreContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<List<BookResponse>>> GetAll()
{
var books = await _context.Books
.Include(b => b.Author)
.Include(b => b.Category)
.ToListAsync();
return books.Select(b => new BookResponse
{
Id = b.Id,
Title = b.Title,
Isbn = b.Isbn,
Price = b.Price,
AuthorName = b.Author.Name,
CategoryName = b.Category != null ? b.Category.Name : "Uncategorized"
}).ToList();
}
[HttpGet("{id}")]
public async Task<ActionResult<BookResponse>> GetById(int id)
{
var book = await _context.Books
.Include(b => b.Author)
.Include(b => b.Category)
.FirstOrDefaultAsync(b => b.Id == id);
if (book == null) return NotFound();
return new BookResponse
{
Id = book.Id,
Title = book.Title,
Isbn = book.Isbn,
Price = book.Price,
AuthorName = book.Author.Name,
CategoryName = book.Category != null ? book.Category.Name : "Uncategorized"
};
}
[HttpPost]
public async Task<ActionResult<BookResponse>> Create(CreateBookRequest request)
{
var author = await _context.Authors.FindAsync(request.AuthorId);
if (author == null) return BadRequest("Author not found");
var book = new Book
{
Title = request.Title,
Isbn = request.Isbn,
Price = request.Price,
Summary = request.Summary,
AuthorId = request.AuthorId,
Author = author,
CategoryId = request.CategoryId
};
_context.Books.Add(book);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = book.Id }, new BookResponse
{
Id = book.Id,
Title = book.Title,
Isbn = book.Isbn,
Price = book.Price,
AuthorName = author.Name,
CategoryName = "Uncategorized"
});
}
[HttpGet("search")]
public async Task<ActionResult<List<BookResponse>>> Search(string query)
{
var books = await _context.Books
.Include(b => b.Author)
.Where(b => b.Title.Contains(query) || b.Author.Name.Contains(query))
.ToListAsync();
return books.Select(b => new BookResponse
{
Id = b.Id,
Title = b.Title,
Isbn = b.Isbn,
Price = b.Price,
AuthorName = b.Author.Name,
CategoryName = b.Category != null ? b.Category.Name : "Uncategorized"
}).ToList();
}
}
- path: "BookStore/Program.cs"
content: |
using Microsoft.EntityFrameworkCore;
using BookStore.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<BookStoreContext>(options =>
options.UseInMemoryDatabase("BookStore"));
var app = builder.Build();
app.MapControllers();
app.Run();
assertions:
- type: "exit_success"
- type: "file_contains"
path: "BookStore/BookStore.csproj"
value: "<Nullable>enable</Nullable>"
- type: "file_contains"
path: "BookStore/Models/Book.cs"
value: "Category?"
- type: "file_contains"
path: "BookStore/Dtos/BookDto.cs"
value: "string?"
rubric:
- "Did the agent handle the required vs optional navigation property distinction? `Book.Author` is a required navigation (non-nullable FK `AuthorId`) and should use `= null!` or stay non-nullable. `Book.Category` is an optional navigation (nullable FK `CategoryId?`) and MUST be `Category?`. Check that the agent did not make both nullable or both non-nullable."
- "Did the agent keep `DbSet<T>` properties non-nullable in BookStoreContext? EF Core always initializes these. They should remain `DbSet<Book>`, `DbSet<Author>`, `DbSet<Category>` without `?`. The agent may add `= null!` or use `=> Set<T>()` but must NOT make them nullable."
- "Did the agent correctly identify that `BookResponse.CategoryName` can be null (category is optional) and make it `string?`, while `BookResponse.AuthorName` should remain non-nullable (author is always present)?"
- "Did the agent handle `Author.Biography` and `Book.Summary` appropriately? These are descriptive text fields that logically could be null. The agent should make them `string?` (nullable) since not every book/author necessarily has a summary/biography, OR use `= string.Empty` / `= null!` with a comment. It should NOT silently leave them as `string` which would make them NOT NULL columns in EF Core."
- "Did the agent recognize the ASP.NET model validation impact? With NRTs enabled, non-nullable string properties on `CreateBookRequest` (like `Title`, `Isbn`) become implicitly `[Required]`. The agent should either accept this (correct for Title/Isbn) or annotate `Summary` as `string?` if it should be optional. Check that the agent did not blindly make all DTO strings nullable."
- "Does the project build with zero CS86xx warnings after the agent's changes?"
timeout: 300