From 0a22892e3fc7a44c4fbfaad5fb855e346336da8f Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 8 Nov 2026 22:59:23 +0700 Subject: [PATCH 1/6] chore(kanban): move task 030 to in-progress --- .../030-bring-ganda-repo-audit-clean/task.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename kanban/{to-do => in-progress}/030-bring-ganda-repo-audit-clean/task.md (100%) diff --git a/kanban/to-do/030-bring-ganda-repo-audit-clean/task.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md similarity index 100% rename from kanban/to-do/030-bring-ganda-repo-audit-clean/task.md rename to kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md From 0eac4ebb378efcf18d751857880aeb38c789b1d7 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 8 Nov 2026 23:08:40 +0700 Subject: [PATCH 2/6] chore(audit): apply ganda repo audit --fix The live audit still failed on glossary, hooks, the Nuru pin, required gitignore entries, vscode settings, the window icon, and pull_request branches. --fix is the baseline, including the Amuru 3 and Terminal pin moves that came with the Nuru bump. why: task 030 --- .githooks/post-checkout | 1 + .githooks/post-checkout.cs | 33 +++++++++++ .githooks/post-commit.cs | 19 ++++-- .githooks/post-merge.cs | 19 ++++-- .githooks/pre-commit | 1 + .githooks/pre-commit.cs | 34 +++++++++++ .githooks/pre-push | 1 + .githooks/pre-push.cs | 104 +++++++++++++++++++++++++++++++++ .github/workflows/workflow.yml | 2 +- .gitignore | 31 ++++++++++ .memsearch.toml | 50 ---------------- .vscode/settings.json | 37 ------------ Directory.Packages.props | 10 ++-- glossary.md | 17 ++++++ 14 files changed, 254 insertions(+), 105 deletions(-) create mode 120000 .githooks/post-checkout create mode 100755 .githooks/post-checkout.cs create mode 120000 .githooks/pre-commit create mode 100755 .githooks/pre-commit.cs create mode 120000 .githooks/pre-push create mode 100755 .githooks/pre-push.cs delete mode 100644 .memsearch.toml delete mode 100644 .vscode/settings.json create mode 100644 glossary.md diff --git a/.githooks/post-checkout b/.githooks/post-checkout new file mode 120000 index 0000000..79fc8cd --- /dev/null +++ b/.githooks/post-checkout @@ -0,0 +1 @@ +post-checkout.cs \ No newline at end of file diff --git a/.githooks/post-checkout.cs b/.githooks/post-checkout.cs new file mode 100755 index 0000000..9e4f06b --- /dev/null +++ b/.githooks/post-checkout.cs @@ -0,0 +1,33 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:package TimeWarp.Amuru.Tools +#:property NoWarn=CA2007 +#:property RunAnalyzers=false + +// Dispatcher for branch checkouts only ($3 == 1): ganda repo attest. +// Exit 0 always. +using TimeWarp.Amuru; + +// git post-checkout +// branch_flag is 1 for branch checkout, 0 for file checkout. +if (args.Length < 3 || args[2] != "1") +{ + return 0; +} + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +if (!string.Equals(Environment.GetEnvironmentVariable("GANDA_ATTEST_HOOK"), "0", StringComparison.Ordinal)) +{ + await Shell.Builder("ganda") + .WithArguments("repo", "attest") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +} + +return 0; diff --git a/.githooks/post-commit.cs b/.githooks/post-commit.cs index a6f0920..41673a6 100755 --- a/.githooks/post-commit.cs +++ b/.githooks/post-commit.cs @@ -2,7 +2,10 @@ #:package TimeWarp.Amuru #:package TimeWarp.Amuru.Tools #:property NoWarn=CA2007 +#:property RunAnalyzers=false +// Dispatcher: ganda repo attest. +// Exit 0 always — commit already landed. using TimeWarp.Amuru; string? root = Git.FindRoot(); @@ -11,9 +14,13 @@ return 0; } -await Shell.Builder("ganda") - .WithArguments("memsearch", "index-repo", "--background") - .WithWorkingDirectory(root) - .WithNoValidation() - .RunAsync(); -return 0; \ No newline at end of file +if (!string.Equals(Environment.GetEnvironmentVariable("GANDA_ATTEST_HOOK"), "0", StringComparison.Ordinal)) +{ + await Shell.Builder("ganda") + .WithArguments("repo", "attest") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +} + +return 0; diff --git a/.githooks/post-merge.cs b/.githooks/post-merge.cs index a6f0920..2a1908a 100755 --- a/.githooks/post-merge.cs +++ b/.githooks/post-merge.cs @@ -2,7 +2,10 @@ #:package TimeWarp.Amuru #:package TimeWarp.Amuru.Tools #:property NoWarn=CA2007 +#:property RunAnalyzers=false +// Dispatcher: ganda repo attest. +// Exit 0 always — cannot undo an already-completed merge. using TimeWarp.Amuru; string? root = Git.FindRoot(); @@ -11,9 +14,13 @@ return 0; } -await Shell.Builder("ganda") - .WithArguments("memsearch", "index-repo", "--background") - .WithWorkingDirectory(root) - .WithNoValidation() - .RunAsync(); -return 0; \ No newline at end of file +if (!string.Equals(Environment.GetEnvironmentVariable("GANDA_ATTEST_HOOK"), "0", StringComparison.Ordinal)) +{ + await Shell.Builder("ganda") + .WithArguments("repo", "attest") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +} + +return 0; diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 120000 index 0000000..4f22eb4 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1 @@ +pre-commit.cs \ No newline at end of file diff --git a/.githooks/pre-commit.cs b/.githooks/pre-commit.cs new file mode 100755 index 0000000..68958ac --- /dev/null +++ b/.githooks/pre-commit.cs @@ -0,0 +1,34 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:package TimeWarp.Amuru.Tools +#:property NoWarn=CA2007;CA1849;RS0030 +#:property RunAnalyzers=false + +// Refuse commits while HEAD is master or main. +// Escape hatch: git commit --no-verify +using TimeWarp.Amuru; + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +CommandOutput revParse = await Shell.Builder("git") + .WithArguments("rev-parse", "--abbrev-ref", "HEAD") + .WithWorkingDirectory(root) + .WithNoValidation() + .CaptureAsync(); + +string branch = revParse.Stdout.Trim(); +if (branch is "master" or "main") +{ +#pragma warning disable RS0030, CA1849 // hook runfile: stderr to git, no ITerminal host + Console.Error.WriteLine($"Refusing commit on '{branch}'."); + Console.Error.WriteLine("Do not commit on master/main — use a feature worktree/branch (e.g. ganda worktree / task branch)."); + Console.Error.WriteLine("Escape hatch (intentional only): git commit --no-verify"); +#pragma warning restore RS0030, CA1849 + return 1; +} + +return 0; diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 120000 index 0000000..ab587a2 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1 @@ +pre-push.cs \ No newline at end of file diff --git a/.githooks/pre-push.cs b/.githooks/pre-push.cs new file mode 100755 index 0000000..537ad61 --- /dev/null +++ b/.githooks/pre-push.cs @@ -0,0 +1,104 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:package TimeWarp.Amuru.Tools +#:property NoWarn=CA2007;CA1849;RS0030 +#:property RunAnalyzers=false + +// Refuse pushes that update home branches (master/main) while HEAD is master or main. +// Allow other dests (feature/*, etc.) so origin-home can publish a missing --into ref. +// Allow refs/tags/* (release tags) and refs/ganda/* (claims CAS) when every dest is one of those. +// Mixed tag + branch batches stay refused while HEAD is home. +// An empty dest list updates no branch. Git omits refs that are already up to date and +// refs it has already rejected as non-fast-forward, and runs this hook on what remains. +// Refusing that empty list hides the non-fast-forward error, so the shared claims push +// (pr merge cleanup and worktree gc) cannot retry while HEAD is master. +// A branch update lists its dest. +// Refuse pushing a local task/* branch to a home branch from any HEAD (a task worktree that +// still tracks origin/). Raw-sha sources (kanban publish merge commit) stay allowed. +// Commits on master stay blocked by pre-commit. Escape hatch: git push --no-verify +using TimeWarp.Amuru; + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +// stdin: (one line per dest) +List remoteRefs = []; +List taskToHome = []; +string? line; +while ((line = Console.In.ReadLine()) is not null) +{ + if (string.IsNullOrWhiteSpace(line)) + continue; + + string[] parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 3) + { + remoteRefs.Add(parts[2]); + if (parts[0].StartsWith("refs/heads/task/", StringComparison.Ordinal) && IsHomeBranchDest(parts[2])) + taskToHome.Add($"{parts[0]["refs/heads/".Length..]} -> {parts[2]["refs/heads/".Length..]}"); + } +} + +if (taskToHome.Count > 0) +{ +#pragma warning disable RS0030, CA1849 // hook runfile: stderr to git, no ITerminal host + foreach (string push in taskToHome) + Console.Error.WriteLine($"Refusing push of task branch to home: {push}."); + Console.Error.WriteLine("Task branches publish to origin/ and land on home via PR."); + Console.Error.WriteLine("Fix tracking: ganda repo audit --fix --checks task-branch-upstream, then git push."); + Console.Error.WriteLine("Escape hatch (intentional only): git push --no-verify"); +#pragma warning restore RS0030, CA1849 + return 1; +} + +// Nothing in this push will update a ref. Allow it so git can report +// "everything up-to-date" or the non-fast-forward rejection it already decided. +if (remoteRefs.Count == 0) +{ + return 0; +} + +if (remoteRefs.All(IsExemptDest)) +{ + return 0; +} + +CommandOutput revParse = await Shell.Builder("git") + .WithArguments("rev-parse", "--abbrev-ref", "HEAD") + .WithWorkingDirectory(root) + .WithNoValidation() + .CaptureAsync(); + +string branch = revParse.Stdout.Trim(); +if (branch is "master" or "main") +{ + bool updatesHome = remoteRefs.Any(IsHomeBranchDest); + bool mixesTagAndBranch = remoteRefs.Any(IsTagDest) && remoteRefs.Any(IsBranchDest); + if (updatesHome || mixesTagAndBranch) + { +#pragma warning disable RS0030, CA1849 // hook runfile: stderr to git, no ITerminal host + Console.Error.WriteLine($"Refusing push while HEAD is '{branch}'."); + Console.Error.WriteLine("Do not push from master/main — use a feature worktree/branch (e.g. ganda worktree / task branch)."); + Console.Error.WriteLine("Escape hatch (intentional only): git push --no-verify"); +#pragma warning restore RS0030, CA1849 + return 1; + } +} + +return 0; + +static bool IsExemptDest(string remoteRef) => + IsTagDest(remoteRef) + || remoteRef.StartsWith("refs/ganda/", StringComparison.Ordinal); + +static bool IsTagDest(string remoteRef) => + remoteRef.StartsWith("refs/tags/", StringComparison.Ordinal); + +static bool IsBranchDest(string remoteRef) => + remoteRef.StartsWith("refs/heads/", StringComparison.Ordinal); + +static bool IsHomeBranchDest(string remoteRef) => + remoteRef is "refs/heads/master" or "refs/heads/main"; diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 8fbcab3..38c65fd 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -14,7 +14,7 @@ on: - 'source/Directory.Build.props' - 'nuget.config' pull_request: - branches: [master] + branches: [master, 'feature/**'] paths: - 'source/**' - 'tests/**' diff --git a/.gitignore b/.gitignore index c553a41..e1a9645 100644 --- a/.gitignore +++ b/.gitignore @@ -317,3 +317,34 @@ __pycache__/ /.aider.tags.cache.v3/cache.db-wal /.aider.input.history .nuget-cache/ + +# Routine journals, mission briefs, and progress logs beside kitchens (local; not product) +*.journal.json +stacked-task-set*.resume.json +*.next.md +*.progress.log + +# Review-round markers beside kitchens (local; not product) +**/review/round-*/base.sha +**/review/round-*/closed + +# Herdr pane-command oracle logs beside kitchens (local; not product) +oracle-*.log + +# Task-work host progress lines beside kitchens (local; not product) +task-work.progress.log + +# Mission heartbeat in the claim worktree (local; not product) +mission.heartbeat + +# VS Code settings: Peacock rewrites this file; vscode-window-icon --fix writes it locally (not tracked) +.vscode/settings.json + +# Per-user tool state (XDG .local/, e.g. gh device-id; local; not product) +.local/ +# AOT dev CLI (self-install output; local; not product) +bin/dev +# AOT dev CLI debug companion (self-install output; local; not product) +bin/dev.dbg +# AOT dev CLI on Windows (self-install output; local; not product) +bin/dev.exe diff --git a/.memsearch.toml b/.memsearch.toml deleted file mode 100644 index 00bcc0e..0000000 --- a/.memsearch.toml +++ /dev/null @@ -1,50 +0,0 @@ -# MemSearch repo knowledge index — TimeWarp convention -# -# SPEC (proposed extension) -# ------------------------- -# memsearch's stock CLI ignores [index]; this file is honored by: -# ganda memsearch index-repo -# ganda hooks install memsearch (post-commit / post-merge runfiles) -# -# Purpose: index markdown knowledge in THIS repo only. One Milvus collection -# per repo (auto-derived from repo path). Global DB file stays at -# ~/.memsearch/milvus.db unless [milvus].uri is overridden below. -# -# [index].paths -# Directory roots (relative to repo root). memsearch indexes all .md files -# under each path recursively. Missing paths are skipped. -# -# [index].exclude_paths -# Directory paths removed from the resolved path list before indexing. -# Does not prune subdirectories when a parent path (e.g. "kanban/") is listed. -# -# [index].exclude_globs -# Reserved — file-level globs (e.g. "**/task-template.md"). Not implemented in v1. -# -# [index].enabled -# false disables hook indexing (conversation plugins still work). -# -# [milvus].collection -# Empty = auto-derive ms__<8char_sha256> from repo root. -# -# Search this repo only: -# memsearch search "query" -c --source-prefix "$(pwd)/kanban/" - -[index] -enabled = true -paths = [ - "kanban", - "documentation", - "okf", - "adr", - ".memsearch/memory", -] -exclude_paths = [] -exclude_globs = [ - "**/task-template.md", - "**/overview.md", -] - -[milvus] -# uri = ".memsearch/milvus.db" # optional per-repo DB file -collection = "" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3201e56..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#82e5ee", - "activityBar.background": "#82e5ee", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#e857dc", - "activityBarBadge.foreground": "#15202b", - "commandCenter.border": "#15202b99", - "sash.hoverBorder": "#82e5ee", - "statusBar.background": "#55dbe8", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#28d1e2", - "statusBarItem.remoteBackground": "#e43ad5", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#55dbe8", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#55dbe899", - "titleBar.inactiveForeground": "#15202b99", - "activityBarTop.activeBackground": "#82e5ee", - "activityBarTop.background": "#82e5ee", - "activityBarTop.foreground": "#15202b", - "activityBarTop.inactiveForeground": "#15202b99", - "commandCenter.foreground": "#15202b", - "statusBar.debuggingBackground": "#55dbe8", - "statusBar.debuggingForeground": "#15202b" - }, - "peacock.remoteColor": "#55dbe8", - "cSpell.words": [ - "assemblyinfo", - "buildtransitive", - "contentfiles", - "nugets" - ], - "window.title": "timewarp-source-generators · ${rootName}${separator}${activeEditorShort}", - "timewarp.blurImagePath": "assets/timewarp-source-generators-avatar.svg" -} diff --git a/Directory.Packages.props b/Directory.Packages.props index b7be34c..2808d7b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,10 +19,10 @@ - - - - - + + + + + diff --git a/glossary.md b/glossary.md new file mode 100644 index 0000000..e91d5fa --- /dev/null +++ b/glossary.md @@ -0,0 +1,17 @@ +# Glossary + +Shared meanings for words this repository uses. + +When an agent or reviewer has to ask what a term or acronym means, or sees one used two ways, the definition goes into `glossary.md` in the same PR. + +## Terms + +| Term | Meaning | +|------|---------| +| _example_ | Replace this row with a repo-specific word and its one meaning. This example row is not a glossary entry. | + +## Acronyms + +| Acronym | Expansion | +|---------|-----------| +| _example_ | Replace this row with a repo-specific short form and what it expands to. This example row is not a glossary entry. | From a936c585b9853c936df21ba49e19e478a4ea9ee5 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 8 Nov 2026 23:15:39 +0700 Subject: [PATCH 3/6] fix(audit): clear the remaining repo audit failures Charset, the self-package analyzer downgrade, and the markdown docs split are the rows --fix cannot take. Nuru 4 moved ICommand and Unit to TimeWarp.Mediator and returns Task, and the Amuru 3 pin is net11-only, so the dev CLI and the git hooks target net11 and CI installs that SDK. why: task 030 --- .editorconfig | 7 +- .githooks/Directory.Build.props | 11 + .github/workflows/workflow.yml | 6 +- .../markdown-docs-generator.cs | 668 ++++-------------- ...markdown-docs-generator.markdown-to-xml.cs | 409 +++++++++++ tools/dev-cli/Directory.Build.props | 3 + tools/dev-cli/endpoints/build-command.cs | 10 +- tools/dev-cli/endpoints/test-command.cs | 10 +- .../endpoints/verify-samples-command.cs | 4 +- tools/dev-cli/endpoints/workflow-command.cs | 12 +- tools/dev-cli/global-usings.cs | 6 +- 11 files changed, 593 insertions(+), 553 deletions(-) create mode 100644 .githooks/Directory.Build.props create mode 100644 source/timewarp-source-generators/markdown-docs-generator.markdown-to-xml.cs diff --git a/.editorconfig b/.editorconfig index 00cca3d..9dfc746 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,6 +23,7 @@ tab_width = 2 # New line preferences end_of_line = lf insert_final_newline = true +charset = utf-8 # ReSharper properties resharper_html_attribute_indent = single_indent @@ -32,7 +33,6 @@ resharper_check_namespace_highlighting= none # Development files [*.{cs,csx,cshtml,csproj,razor,sln,props,targets,json,yml,gitignore}] -charset = "utf-8" trim_trailing_whitespace = true #### .NET Coding Conventions #### @@ -291,6 +291,11 @@ dotnet_diagnostic.ca1308.severity = none # ganda repo audit (https://github.com/TimeWarpEngineering/timewarp-ganda) # ═══════════════════════════════════════════════════════════════════════════════ [ganda.audit] +# This repo is the TimeWarp.SourceGenerators package. A root PackageReference +# would consume the published package from the project that produces it. +# TW0007 is implemented and tested here (task 028). Task 030. +global-usings-analyzer.severity = off + # kebab-path-names prune (additive, basename match): # .nuget-cache = local package cache (gitignored; package basenames are not ours) # generated / .generated / build = generator/build outputs diff --git a/.githooks/Directory.Build.props b/.githooks/Directory.Build.props new file mode 100644 index 0000000..6c193e6 --- /dev/null +++ b/.githooks/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + net11.0 + + diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 38c65fd..69077b3 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -49,7 +49,11 @@ jobs: fetch-depth: 0 - uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' + # Dev CLI targets net11 because the audit pin TimeWarp.Amuru 3.0.0-beta.1 is net11-only. + # Net10 stays installed so the test projects still build. Task 030. + dotnet-version: | + 10.0.x + 11.0.100-rc.3.26522.148 # Prefer NuGet Trusted Publishing (OIDC). Fall back to org API key when no trust # policy exists yet for this package (common on first release under a new policy). diff --git a/source/timewarp-source-generators/markdown-docs-generator.cs b/source/timewarp-source-generators/markdown-docs-generator.cs index 9573184..8d36b37 100644 --- a/source/timewarp-source-generators/markdown-docs-generator.cs +++ b/source/timewarp-source-generators/markdown-docs-generator.cs @@ -1,532 +1,136 @@ -namespace TimeWarp.SourceGenerators; - -[Generator] -public class MarkdownDocsGenerator : IIncrementalGenerator -{ - // Regex pattern for checking if a file name is kebab-case - private static readonly Regex KebabCasePattern = new(@"^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", RegexOptions.Compiled); - private static readonly DiagnosticDescriptor MarkdownDocsGeneratorLoadedDescriptor = new( - id: "TW0003", - title: "MarkdownDocs Generator Loaded", - messageFormat: "The MarkdownDocs generator has been loaded and initialized", - category: "SourceGenerator", - DiagnosticSeverity.Info, - isEnabledByDefault: true - ); - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - // Report initialization diagnostic - IncrementalValueProvider initializationTrigger = context.CompilationProvider - .Select((compilation, _) => true); - - context.RegisterSourceOutput(initializationTrigger, (sourceContext, _) => - { - sourceContext.ReportDiagnostic( - Diagnostic.Create(MarkdownDocsGeneratorLoadedDescriptor, Location.None) - ); - }); - - // Find all class declarations in C# files - IncrementalValuesProvider<(ClassDeclarationSyntax ClassDeclaration, string? Namespace, string FilePath)> classDeclarations = - context.SyntaxProvider - .CreateSyntaxProvider( - predicate: (s, _) => s is ClassDeclarationSyntax, - transform: (ctx, _) => - { - var classDeclaration = (ClassDeclarationSyntax)ctx.Node; - var namespaceDecl = classDeclaration.Ancestors().OfType().FirstOrDefault(); - var filePath = ctx.Node.SyntaxTree.FilePath; - return (classDeclaration, namespaceDecl?.Name.ToString(), filePath); - }); - - // Find all .md files - IncrementalValuesProvider markdownFiles = context.AdditionalTextsProvider - .Where(file => file.Path.EndsWith(".md", StringComparison.OrdinalIgnoreCase)); - - // Group class declarations by class name to avoid duplicates - var groupedClasses = classDeclarations - .Collect() - .Select((classes, _) => classes - .GroupBy(c => (c.ClassDeclaration.Identifier.Text, c.Namespace)) - .Select(g => g.First()) - .ToArray()); - - // Combine into pairs - var pairs = groupedClasses.Combine(markdownFiles.Collect()); - - // Generate documentation for matching pairs - context.RegisterSourceOutput(pairs, (sourceContext, pair) => - { - var (classGroups, markdownTexts) = pair; - - foreach (var (classDeclaration, namespaceName, filePath) in classGroups) - { - var className = classDeclaration.Identifier.Text; - - // Extract source file name without extension - var sourceFileName = Path.GetFileNameWithoutExtension(filePath); - - // Find matching markdown file using appropriate strategy - AdditionalText? matchingMd = null; - - // If source file is kebab-case, try to match kebab-case markdown first - if (IsKebabCase(sourceFileName)) - { - matchingMd = markdownTexts.FirstOrDefault(md => - Path.GetFileNameWithoutExtension(md.Path).Equals(sourceFileName, StringComparison.OrdinalIgnoreCase)); - } - - // Fall back to class name match (for backward compatibility or PascalCase files) - if (matchingMd == null) - { - matchingMd = markdownTexts.FirstOrDefault(md => - Path.GetFileNameWithoutExtension(md.Path).Equals(className, StringComparison.OrdinalIgnoreCase)); - } - - // Also try kebab-case of the class name (PascalCase source + kebab markdown) - if (matchingMd == null) - { - string kebabClassName = ConvertToKebabCase(className); - matchingMd = markdownTexts.FirstOrDefault(md => - Path.GetFileNameWithoutExtension(md.Path).Equals(kebabClassName, StringComparison.OrdinalIgnoreCase)); - } - - if (matchingMd != null) - { - var markdownContent = matchingMd.GetText()?.ToString() ?? string.Empty; - var (classDocs, methodDocs, propertyDocs) = ConvertMarkdownToXmlDocs(markdownContent, classDeclaration); - - var sourceText = SourceText.From($@"// Auto-generated documentation for {className} -#nullable enable - -{(namespaceName != null ? $"namespace {namespaceName};" : "")} - -{classDocs} -public partial class {className} -{{ -{propertyDocs} -{methodDocs} -}} -", Encoding.UTF8); - - sourceContext.AddSource($"{className}.docs.g.cs", sourceText); - } - } - }); - } - - private static (string ClassDocs, string MethodDocs, string PropertyDocs) ConvertMarkdownToXmlDocs(string markdownContent, ClassDeclarationSyntax classDeclaration) - { - var classBuilder = new StringBuilder(); - var methodBuilder = new StringBuilder(); - var propertyBuilder = new StringBuilder(); - var reader = new StringReader(markdownContent); - string? line; - - // State tracking - var currentSection = ""; - var contentBuilder = new StringBuilder(); - var inMethodSection = false; - var inPropertySection = false; - var currentMethod = ""; - var currentMethodDescription = ""; - var currentProperty = ""; - var currentPropertyDescription = ""; - - // Get method signatures from the class declaration - var methodSignatures = classDeclaration.Members - .OfType() - .ToDictionary( - m => m.Identifier.Text, - m => $"public partial {m.ReturnType} {m.Identifier}({string.Join(", ", m.ParameterList.Parameters.Select(p => $"{p.Type} {p.Identifier}{(p.Default != null ? " = " + p.Default.Value.ToString() : "")}"))})" - ); - - // Get property signatures from the class declaration (only partial properties) - var propertySignatures = classDeclaration.Members - .OfType() - .Where(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - .ToDictionary( - p => p.Identifier.Text, - p => - { - var modifiers = string.Join(" ", p.Modifiers.Select(m => m.Text)); - var accessors = new List(); - - if (p.AccessorList != null) - { - foreach (var accessor in p.AccessorList.Accessors) - { - if (accessor.Keyword.Text == "get") - { - // For non-nullable reference types, initialize with default - var typeStr = p.Type?.ToString() ?? ""; - if (typeStr == "string" && !typeStr.EndsWith("?")) - { - accessors.Add("get => field ??= \"\""); - } - else - { - accessors.Add("get => field"); - } - } - else if (accessor.Keyword.Text == "set") - { - accessors.Add("set => field = value"); - } - else if (accessor.Keyword.Text == "init") - { - accessors.Add("init => field = value"); - } - } - } - - var accessorString = string.Join("; ", accessors); - return $"{modifiers} {p.Type} {p.Identifier} {{ {accessorString}; }}"; - } - ); - - while ((line = reader.ReadLine()) != null) - { - if (line.StartsWith("# ")) // Class name - skip - continue; - - if (line.StartsWith("## ")) - { - // Process previous section - if (inMethodSection) - ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); - else if (inPropertySection) - ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); - else - ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); - - // Start new section - currentSection = line.Substring(3).Trim(); - inMethodSection = currentSection == "Methods"; - inPropertySection = currentSection == "Properties"; - contentBuilder.Clear(); - currentMethodDescription = ""; - currentPropertyDescription = ""; - continue; - } - - if (line.StartsWith("### ") && inMethodSection) - { - // Process previous method if exists - if (!string.IsNullOrEmpty(currentMethod)) - { - ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); - } - - // Start new method - currentMethod = line.Substring(4).Trim(); - contentBuilder.Clear(); - currentMethodDescription = ""; - continue; - } - - if (line.StartsWith("### ") && inPropertySection) - { - // Process previous property if exists - if (!string.IsNullOrEmpty(currentProperty)) - { - ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); - } - - // Start new property - currentProperty = line.Substring(4).Trim(); - contentBuilder.Clear(); - currentPropertyDescription = ""; - continue; - } - - // If we haven't hit any section yet, this is the class summary - if (string.IsNullOrEmpty(currentSection) && !string.IsNullOrWhiteSpace(line)) - { - classBuilder.AppendLine("/// "); - classBuilder.AppendLine($"/// {line.Trim()}"); - classBuilder.AppendLine("/// "); - continue; - } - - // Capture method description (text before any #### subsections) - if (inMethodSection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentMethodDescription)) - { - currentMethodDescription = line.Trim(); - continue; - } - - // Capture property description (text before any #### subsections) - if (inPropertySection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentPropertyDescription)) - { - currentPropertyDescription = line.Trim(); - continue; - } - - // Add content to current section - if (!string.IsNullOrWhiteSpace(line)) - { - contentBuilder.AppendLine(line); - } - } - - // Process the last section - if (inMethodSection) - ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); - else if (inPropertySection) - ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); - else - ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); - - return (classBuilder.ToString(), methodBuilder.ToString(), propertyBuilder.ToString()); - } - - private static void ProcessSection(StringBuilder builder, string section, string content) - { - if (string.IsNullOrEmpty(content)) - return; - - switch (section) - { - case "Remarks": - builder.AppendLine("/// "); - foreach (var line in content.Split('\n')) - { - builder.AppendLine($"/// {line.Trim()}"); - } - builder.AppendLine("/// "); - break; - - case "See Also": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"\[(.*?)\]\((.*?)\)"); - if (match.Success) - { - var type = match.Groups[1].Value; - // Convert angle brackets to curly braces for XML documentation - var xmlType = type.Replace('<', '{').Replace('>', '}'); - builder.AppendLine($"/// "); - } - } - break; - - case "Inheritance": - if (content.Contains("@")) - { - var match = Regex.Match(content, @"@(\S+)"); - if (match.Success) - { - var type = match.Groups[1].Value; - // Extract the type name and keep T as the generic parameter - var baseType = type.Split('<')[0]; - builder.AppendLine($"/// "); - } - } - break; - - case "References": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"@(\S+)"); - if (match.Success) - { - var type = match.Groups[1].Value; - // Handle fully qualified type names - if (!type.Contains(".")) - { - type = "System." + type; - } - builder.AppendLine($"/// "); - } - } - break; - } - } - - private static void ProcessMethodSection(StringBuilder builder, string methodName, string description, string content, Dictionary methodSignatures) - { - if (string.IsNullOrEmpty(methodName)) - return; - - // Handle constructor specially - if (methodName == "Constructor") - methodName = methodSignatures.Keys.FirstOrDefault(k => k.Contains("ctor")) ?? ""; - - if (!methodSignatures.ContainsKey(methodName)) - return; - - builder.AppendLine($" // Documentation for {methodName}"); - - // Add method summary if available - if (!string.IsNullOrEmpty(description)) - { - builder.AppendLine(" /// "); - builder.AppendLine($" /// {description}"); - builder.AppendLine(" /// "); - } - - var reader = new StringReader(content); - string? line; - var currentSubSection = ""; - var subSectionContent = new StringBuilder(); - - while ((line = reader.ReadLine()) != null) - { - if (line.StartsWith("#### ")) - { - // Process previous subsection - ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - - // Start new subsection - currentSubSection = line.Substring(5).Trim(); - subSectionContent.Clear(); - continue; - } - - // Add content to current subsection - if (!string.IsNullOrWhiteSpace(line)) - { - subSectionContent.AppendLine(line); - } - } - - // Process the last subsection - ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - - // Add the method signature with a semicolon - builder.AppendLine($" {methodSignatures[methodName]};"); - builder.AppendLine(); - } - - private static void ProcessMethodSubSection(StringBuilder builder, string subSection, string content) - { - if (string.IsNullOrEmpty(content)) - return; - - switch (subSection) - { - case "Parameters": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); - if (match.Success) - { - builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); - } - } - break; - - case "Returns": - builder.AppendLine(" /// "); - foreach (var line in content.Split('\n')) - { - builder.AppendLine($" /// {line.Trim()}"); - } - builder.AppendLine(" /// "); - break; - - case "Exceptions": - foreach (var line in content.Split('\n')) - { - if (string.IsNullOrWhiteSpace(line)) continue; - var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); - if (match.Success) - { - builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); - } - } - break; - } - } - - private static void ProcessPropertySection(StringBuilder builder, string propertyName, string description, string content, Dictionary propertySignatures) - { - if (string.IsNullOrEmpty(propertyName)) - return; - - if (!propertySignatures.ContainsKey(propertyName)) - return; - - builder.AppendLine($" // Documentation for {propertyName}"); - - // Add property summary if available - if (!string.IsNullOrEmpty(description)) - { - builder.AppendLine(" /// "); - builder.AppendLine($" /// {description}"); - builder.AppendLine(" /// "); - } - - var reader = new StringReader(content); - string? line; - var currentSubSection = ""; - var subSectionContent = new StringBuilder(); - - while ((line = reader.ReadLine()) != null) - { - if (line.StartsWith("#### ")) - { - // Process previous subsection - ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - - // Start new subsection - currentSubSection = line.Substring(5).Trim(); - subSectionContent.Clear(); - continue; - } - - // Add content to current subsection - if (!string.IsNullOrWhiteSpace(line)) - { - subSectionContent.AppendLine(line); - } - } - - // Process the last subsection - ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); - - // Add the property signature - builder.AppendLine($" {propertySignatures[propertyName]}"); - builder.AppendLine(); - } - - private static void ProcessPropertySubSection(StringBuilder builder, string subSection, string content) - { - if (string.IsNullOrEmpty(content)) - return; - - switch (subSection) - { - case "Value": - builder.AppendLine(" /// "); - foreach (var line in content.Split('\n')) - { - builder.AppendLine($" /// {line.Trim()}"); - } - builder.AppendLine(" /// "); - break; - } - } - - private static string ConvertToKebabCase(string pascalCase) - { - if (string.IsNullOrEmpty(pascalCase)) - return pascalCase; - - // Insert hyphens before uppercase letters (except the first character) - var kebabCase = Regex.Replace(pascalCase, "(? initializationTrigger = context.CompilationProvider + .Select((compilation, _) => true); + + context.RegisterSourceOutput(initializationTrigger, (sourceContext, _) => + { + sourceContext.ReportDiagnostic( + Diagnostic.Create(MarkdownDocsGeneratorLoadedDescriptor, Location.None) + ); + }); + + // Find all class declarations in C# files + IncrementalValuesProvider<(ClassDeclarationSyntax ClassDeclaration, string? Namespace, string FilePath)> classDeclarations = + context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (s, _) => s is ClassDeclarationSyntax, + transform: (ctx, _) => + { + var classDeclaration = (ClassDeclarationSyntax)ctx.Node; + var namespaceDecl = classDeclaration.Ancestors().OfType().FirstOrDefault(); + var filePath = ctx.Node.SyntaxTree.FilePath; + return (classDeclaration, namespaceDecl?.Name.ToString(), filePath); + }); + + // Find all .md files + IncrementalValuesProvider markdownFiles = context.AdditionalTextsProvider + .Where(file => file.Path.EndsWith(".md", StringComparison.OrdinalIgnoreCase)); + + // Group class declarations by class name to avoid duplicates + var groupedClasses = classDeclarations + .Collect() + .Select((classes, _) => classes + .GroupBy(c => (c.ClassDeclaration.Identifier.Text, c.Namespace)) + .Select(g => g.First()) + .ToArray()); + + // Combine into pairs + var pairs = groupedClasses.Combine(markdownFiles.Collect()); + + // Generate documentation for matching pairs + context.RegisterSourceOutput(pairs, (sourceContext, pair) => + { + var (classGroups, markdownTexts) = pair; + + foreach (var (classDeclaration, namespaceName, filePath) in classGroups) + { + var className = classDeclaration.Identifier.Text; + + // Extract source file name without extension + var sourceFileName = Path.GetFileNameWithoutExtension(filePath); + + // Find matching markdown file using appropriate strategy + AdditionalText? matchingMd = null; + + // If source file is kebab-case, try to match kebab-case markdown first + if (IsKebabCase(sourceFileName)) + { + matchingMd = markdownTexts.FirstOrDefault(md => + Path.GetFileNameWithoutExtension(md.Path).Equals(sourceFileName, StringComparison.OrdinalIgnoreCase)); + } + + // Fall back to class name match (for backward compatibility or PascalCase files) + if (matchingMd == null) + { + matchingMd = markdownTexts.FirstOrDefault(md => + Path.GetFileNameWithoutExtension(md.Path).Equals(className, StringComparison.OrdinalIgnoreCase)); + } + + // Also try kebab-case of the class name (PascalCase source + kebab markdown) + if (matchingMd == null) + { + string kebabClassName = ConvertToKebabCase(className); + matchingMd = markdownTexts.FirstOrDefault(md => + Path.GetFileNameWithoutExtension(md.Path).Equals(kebabClassName, StringComparison.OrdinalIgnoreCase)); + } + + if (matchingMd != null) + { + var markdownContent = matchingMd.GetText()?.ToString() ?? string.Empty; + var (classDocs, methodDocs, propertyDocs) = ConvertMarkdownToXmlDocs(markdownContent, classDeclaration); + + var sourceText = SourceText.From($@"// Auto-generated documentation for {className} +#nullable enable + +{(namespaceName != null ? $"namespace {namespaceName};" : "")} + +{classDocs} +public partial class {className} +{{ +{propertyDocs} +{methodDocs} +}} +", Encoding.UTF8); + + sourceContext.AddSource($"{className}.docs.g.cs", sourceText); + } + } + }); + } + + + private static string ConvertToKebabCase(string pascalCase) + { + if (string.IsNullOrEmpty(pascalCase)) + return pascalCase; + + // Insert hyphens before uppercase letters (except the first character) + var kebabCase = Regex.Replace(pascalCase, "(?() + .ToDictionary( + m => m.Identifier.Text, + m => $"public partial {m.ReturnType} {m.Identifier}({string.Join(", ", m.ParameterList.Parameters.Select(p => $"{p.Type} {p.Identifier}{(p.Default != null ? " = " + p.Default.Value.ToString() : "")}"))})" + ); + + // Get property signatures from the class declaration (only partial properties) + var propertySignatures = classDeclaration.Members + .OfType() + .Where(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + .ToDictionary( + p => p.Identifier.Text, + p => + { + var modifiers = string.Join(" ", p.Modifiers.Select(m => m.Text)); + var accessors = new List(); + + if (p.AccessorList != null) + { + foreach (var accessor in p.AccessorList.Accessors) + { + if (accessor.Keyword.Text == "get") + { + // For non-nullable reference types, initialize with default + var typeStr = p.Type?.ToString() ?? ""; + if (typeStr == "string" && !typeStr.EndsWith("?")) + { + accessors.Add("get => field ??= \"\""); + } + else + { + accessors.Add("get => field"); + } + } + else if (accessor.Keyword.Text == "set") + { + accessors.Add("set => field = value"); + } + else if (accessor.Keyword.Text == "init") + { + accessors.Add("init => field = value"); + } + } + } + + var accessorString = string.Join("; ", accessors); + return $"{modifiers} {p.Type} {p.Identifier} {{ {accessorString}; }}"; + } + ); + + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("# ")) // Class name - skip + continue; + + if (line.StartsWith("## ")) + { + // Process previous section + if (inMethodSection) + ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); + else if (inPropertySection) + ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); + else + ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); + + // Start new section + currentSection = line.Substring(3).Trim(); + inMethodSection = currentSection == "Methods"; + inPropertySection = currentSection == "Properties"; + contentBuilder.Clear(); + currentMethodDescription = ""; + currentPropertyDescription = ""; + continue; + } + + if (line.StartsWith("### ") && inMethodSection) + { + // Process previous method if exists + if (!string.IsNullOrEmpty(currentMethod)) + { + ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); + } + + // Start new method + currentMethod = line.Substring(4).Trim(); + contentBuilder.Clear(); + currentMethodDescription = ""; + continue; + } + + if (line.StartsWith("### ") && inPropertySection) + { + // Process previous property if exists + if (!string.IsNullOrEmpty(currentProperty)) + { + ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); + } + + // Start new property + currentProperty = line.Substring(4).Trim(); + contentBuilder.Clear(); + currentPropertyDescription = ""; + continue; + } + + // If we haven't hit any section yet, this is the class summary + if (string.IsNullOrEmpty(currentSection) && !string.IsNullOrWhiteSpace(line)) + { + classBuilder.AppendLine("/// "); + classBuilder.AppendLine($"/// {line.Trim()}"); + classBuilder.AppendLine("/// "); + continue; + } + + // Capture method description (text before any #### subsections) + if (inMethodSection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentMethodDescription)) + { + currentMethodDescription = line.Trim(); + continue; + } + + // Capture property description (text before any #### subsections) + if (inPropertySection && !string.IsNullOrWhiteSpace(line) && !line.StartsWith("####") && string.IsNullOrEmpty(currentPropertyDescription)) + { + currentPropertyDescription = line.Trim(); + continue; + } + + // Add content to current section + if (!string.IsNullOrWhiteSpace(line)) + { + contentBuilder.AppendLine(line); + } + } + + // Process the last section + if (inMethodSection) + ProcessMethodSection(methodBuilder, currentMethod, currentMethodDescription, contentBuilder.ToString().Trim(), methodSignatures); + else if (inPropertySection) + ProcessPropertySection(propertyBuilder, currentProperty, currentPropertyDescription, contentBuilder.ToString().Trim(), propertySignatures); + else + ProcessSection(classBuilder, currentSection, contentBuilder.ToString().Trim()); + + return (classBuilder.ToString(), methodBuilder.ToString(), propertyBuilder.ToString()); + } + + private static void ProcessSection(StringBuilder builder, string section, string content) + { + if (string.IsNullOrEmpty(content)) + return; + + switch (section) + { + case "Remarks": + builder.AppendLine("/// "); + foreach (var line in content.Split('\n')) + { + builder.AppendLine($"/// {line.Trim()}"); + } + builder.AppendLine("/// "); + break; + + case "See Also": + foreach (var line in content.Split('\n')) + { + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"\[(.*?)\]\((.*?)\)"); + if (match.Success) + { + var type = match.Groups[1].Value; + // Convert angle brackets to curly braces for XML documentation + var xmlType = type.Replace('<', '{').Replace('>', '}'); + builder.AppendLine($"/// "); + } + } + break; + + case "Inheritance": + if (content.Contains("@")) + { + var match = Regex.Match(content, @"@(\S+)"); + if (match.Success) + { + var type = match.Groups[1].Value; + // Extract the type name and keep T as the generic parameter + var baseType = type.Split('<')[0]; + builder.AppendLine($"/// "); + } + } + break; + + case "References": + foreach (var line in content.Split('\n')) + { + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"@(\S+)"); + if (match.Success) + { + var type = match.Groups[1].Value; + // Handle fully qualified type names + if (!type.Contains(".")) + { + type = "System." + type; + } + builder.AppendLine($"/// "); + } + } + break; + } + } + + private static void ProcessMethodSection(StringBuilder builder, string methodName, string description, string content, Dictionary methodSignatures) + { + if (string.IsNullOrEmpty(methodName)) + return; + + // Handle constructor specially + if (methodName == "Constructor") + methodName = methodSignatures.Keys.FirstOrDefault(k => k.Contains("ctor")) ?? ""; + + if (!methodSignatures.ContainsKey(methodName)) + return; + + builder.AppendLine($" // Documentation for {methodName}"); + + // Add method summary if available + if (!string.IsNullOrEmpty(description)) + { + builder.AppendLine(" /// "); + builder.AppendLine($" /// {description}"); + builder.AppendLine(" /// "); + } + + var reader = new StringReader(content); + string? line; + var currentSubSection = ""; + var subSectionContent = new StringBuilder(); + + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("#### ")) + { + // Process previous subsection + ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + + // Start new subsection + currentSubSection = line.Substring(5).Trim(); + subSectionContent.Clear(); + continue; + } + + // Add content to current subsection + if (!string.IsNullOrWhiteSpace(line)) + { + subSectionContent.AppendLine(line); + } + } + + // Process the last subsection + ProcessMethodSubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + + // Add the method signature with a semicolon + builder.AppendLine($" {methodSignatures[methodName]};"); + builder.AppendLine(); + } + + private static void ProcessMethodSubSection(StringBuilder builder, string subSection, string content) + { + if (string.IsNullOrEmpty(content)) + return; + + switch (subSection) + { + case "Parameters": + foreach (var line in content.Split('\n')) + { + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); + if (match.Success) + { + builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); + } + } + break; + + case "Returns": + builder.AppendLine(" /// "); + foreach (var line in content.Split('\n')) + { + builder.AppendLine($" /// {line.Trim()}"); + } + builder.AppendLine(" /// "); + break; + + case "Exceptions": + foreach (var line in content.Split('\n')) + { + if (string.IsNullOrWhiteSpace(line)) continue; + var match = Regex.Match(line, @"`(.*?)`\s*-\s*(.*)"); + if (match.Success) + { + builder.AppendLine($" /// {match.Groups[2].Value.Trim()}"); + } + } + break; + } + } + + private static void ProcessPropertySection(StringBuilder builder, string propertyName, string description, string content, Dictionary propertySignatures) + { + if (string.IsNullOrEmpty(propertyName)) + return; + + if (!propertySignatures.ContainsKey(propertyName)) + return; + + builder.AppendLine($" // Documentation for {propertyName}"); + + // Add property summary if available + if (!string.IsNullOrEmpty(description)) + { + builder.AppendLine(" /// "); + builder.AppendLine($" /// {description}"); + builder.AppendLine(" /// "); + } + + var reader = new StringReader(content); + string? line; + var currentSubSection = ""; + var subSectionContent = new StringBuilder(); + + while ((line = reader.ReadLine()) != null) + { + if (line.StartsWith("#### ")) + { + // Process previous subsection + ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + + // Start new subsection + currentSubSection = line.Substring(5).Trim(); + subSectionContent.Clear(); + continue; + } + + // Add content to current subsection + if (!string.IsNullOrWhiteSpace(line)) + { + subSectionContent.AppendLine(line); + } + } + + // Process the last subsection + ProcessPropertySubSection(builder, currentSubSection, subSectionContent.ToString().Trim()); + + // Add the property signature + builder.AppendLine($" {propertySignatures[propertyName]}"); + builder.AppendLine(); + } + + private static void ProcessPropertySubSection(StringBuilder builder, string subSection, string content) + { + if (string.IsNullOrEmpty(content)) + return; + + switch (subSection) + { + case "Value": + builder.AppendLine(" /// "); + foreach (var line in content.Split('\n')) + { + builder.AppendLine($" /// {line.Trim()}"); + } + builder.AppendLine(" /// "); + break; + } + } +} diff --git a/tools/dev-cli/Directory.Build.props b/tools/dev-cli/Directory.Build.props index 29f5420..99d3af3 100644 --- a/tools/dev-cli/Directory.Build.props +++ b/tools/dev-cli/Directory.Build.props @@ -2,6 +2,9 @@ + + net11.0 + $(NoWarn);CA1031;CA1303;CA1508;CA1515;CA1849;CA2007;CA2016;CA2000;CA1034;CA5399;IDE0065;IL2026;IL2104;IL3050;IL3053;RCS1046;IDE0005;IDE0022;IDE0046;IDE0052;IDE0055;IDE0058;IDE0066;IDE0078;IDE0160;IDE0211;IDE0290 diff --git a/tools/dev-cli/endpoints/build-command.cs b/tools/dev-cli/endpoints/build-command.cs index a20f772..171e843 100644 --- a/tools/dev-cli/endpoints/build-command.cs +++ b/tools/dev-cli/endpoints/build-command.cs @@ -31,17 +31,17 @@ public Handler(ITerminal terminal) Terminal = terminal; } - public async ValueTask Handle(BuildCommand command, CancellationToken ct) + public async Task Handle(BuildCommand command, CancellationToken ct) { Command = command; Ct = ct; - if (!FindRepoRoot()) return Value; - if (!await CleanAsync()) return Value; - if (!await BuildAsync()) return Value; + if (!FindRepoRoot()) return Unit.Value; + if (!await CleanAsync()) return Unit.Value; + if (!await BuildAsync()) return Unit.Value; Terminal.WriteLine("\nBuild completed successfully!".Green()); - return Value; + return Unit.Value; } private bool FindRepoRoot() diff --git a/tools/dev-cli/endpoints/test-command.cs b/tools/dev-cli/endpoints/test-command.cs index 96203b6..5b7aca8 100644 --- a/tools/dev-cli/endpoints/test-command.cs +++ b/tools/dev-cli/endpoints/test-command.cs @@ -34,17 +34,17 @@ public Handler(ITerminal terminal) Terminal = terminal; } - public async ValueTask Handle(TestCommand command, CancellationToken ct) + public async Task Handle(TestCommand command, CancellationToken ct) { Command = command; Ct = ct; - if (!FindRepoRoot()) return Value; - if (!await BuildTestProjectAsync()) return Value; - if (!await RunTestProjectAsync()) return Value; + if (!FindRepoRoot()) return Unit.Value; + if (!await BuildTestProjectAsync()) return Unit.Value; + if (!await RunTestProjectAsync()) return Unit.Value; Terminal.WriteLine("\nTests completed successfully!".Green()); - return Value; + return Unit.Value; } private bool FindRepoRoot() diff --git a/tools/dev-cli/endpoints/verify-samples-command.cs b/tools/dev-cli/endpoints/verify-samples-command.cs index c9c03f9..39a9f4d 100644 --- a/tools/dev-cli/endpoints/verify-samples-command.cs +++ b/tools/dev-cli/endpoints/verify-samples-command.cs @@ -19,12 +19,12 @@ public Handler(ITerminal terminal) Terminal = terminal; } - public ValueTask Handle(VerifySamplesCommand command, CancellationToken ct) + public Task Handle(VerifySamplesCommand command, CancellationToken ct) { Terminal.WriteLine("Verifying samples..."); // TODO: Implement sample verification logic specific to this repo Terminal.WriteLine("Samples verified successfully!"); - return ValueTask.FromResult(Value); + return Task.FromResult(Unit.Value); } } } diff --git a/tools/dev-cli/endpoints/workflow-command.cs b/tools/dev-cli/endpoints/workflow-command.cs index 5ffd150..8f3b89b 100644 --- a/tools/dev-cli/endpoints/workflow-command.cs +++ b/tools/dev-cli/endpoints/workflow-command.cs @@ -46,11 +46,11 @@ INuGetPackageService nuGetPackageService NuGetPackageService = nuGetPackageService; } - public async ValueTask Handle(WorkflowCommand command, CancellationToken ct) + public async Task Handle(WorkflowCommand command, CancellationToken ct) { Ct = ct; - if (!FindRepoRoot()) return Value; + if (!FindRepoRoot()) return Unit.Value; CiMode mode = DetermineMode(command.Mode); Terminal.WriteLine($"Starting CI workflow (mode: {mode})..."); @@ -67,7 +67,7 @@ public async ValueTask Handle(WorkflowCommand command, CancellationToken c if (Environment.ExitCode == 0) Terminal.WriteLine("\nWorkflow completed successfully!".Green()); - return Value; + return Unit.Value; } private bool FindRepoRoot() @@ -135,7 +135,7 @@ private async Task RunReleaseWorkflowAsync(string? apiKey) await PushPackageAsync(version, apiKey); } - private async ValueTask CleanAsync() + private async Task CleanAsync() { Terminal.WriteLine("Cleaning repository artifacts..."); CleanResult result = await RepoCleanService.CleanAsync(Ct); @@ -143,10 +143,10 @@ private async ValueTask CleanAsync() ( $"Clean complete: {result.ObjDirectoriesDeleted} obj, {result.BinDirectoriesDeleted} bin, {result.RootBinFilesCleaned} root-bin items." ); - return Value; + return Unit.Value; } - private async Task RunStepAsync(string title, Func> step) + private async Task RunStepAsync(string title, Func> step) { Terminal.WriteLine($"\n=== {title} ==="); await step(); diff --git a/tools/dev-cli/global-usings.cs b/tools/dev-cli/global-usings.cs index 9052391..a05786d 100644 --- a/tools/dev-cli/global-usings.cs +++ b/tools/dev-cli/global-usings.cs @@ -1,6 +1,10 @@ #region Purpose // Global usings for dev-cli #endregion +#region Design +// ICommand, ICommandHandler, and Unit live in TimeWarp.Mediator as of Nuru 4. +// A static using of Unit collides with Task inside the DevCli package sources, so handlers return Unit.Value. +#endregion global using System; global using System.IO; @@ -14,8 +18,8 @@ global using System.Xml.Linq; global using System.Text.RegularExpressions; +global using TimeWarp.Mediator; global using TimeWarp.Nuru; -global using static TimeWarp.Nuru.Unit; global using TimeWarp.Amuru; global using TimeWarp.Terminal; global using DevCli; From d5144cacd514f2fc9b412b7cd04bcac06ae9e7e6 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 8 Nov 2026 23:20:29 +0700 Subject: [PATCH 4/6] docs(kanban): record task 030 audit results The audit is clean and the validation commands have been run. Task 027 is archived on master so it does not edit the same gitignore. why: task 030 --- .../030-bring-ganda-repo-audit-clean/task.md | 84 ++++++++++++++++--- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md index 320c19f..55437a4 100644 --- a/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md @@ -1,5 +1,21 @@ # Bring ganda repo audit clean +## Goal (implementer restatement) + +`ganda repo audit` on this repo currently fails. The goal is a clean audit (exit 0, no Failed row) on the claimed task branch, with the product still building. + +Acceptance: + +- Re-read the audit at the start and treat that live table as the list, because pins move. +- Apply `ganda repo audit --fix` for every fixable row except `global-usings-analyzer`, and commit that diff. Do not revert a major Nuru or DevCli bump. +- For `global-usings-analyzer`, turn the check off in `[ganda.audit]` with the reason comment from the kitchen. Do not add a root `PackageReference` to `TimeWarp.SourceGenerators`, because this repo is that package. +- Put unquoted `charset = utf-8` under `[*]`. +- Split `markdown-docs-generator.cs` by responsibility so every file is at most 500 lines. No `part-N` partial names. Analyzer behavior stays. +- Bump `TimeWarp.Nuru` and `TimeWarp.Nuru.DevCli` to the version the audit calls latest, and make `./bin/dev build` succeed. `dev --help` still shows `release`, and `dev check-version` still sees `TimeWarp.SourceGenerators`. +- `pull_request.branches` includes `feature/**`. +- After the journal ignore is in `.gitignore`, archive task 027 so it does not edit the same file. +- Record `## Results` and a non-empty `### How to validate` (Smoke + Expect). Stop before `kanban done` and `gh pr create`. + ## Description `ganda repo audit` on `timewarp-source-generators` master fails. @@ -48,26 +64,27 @@ These rows reported Fixable YES. Commit the `--fix` output. Do not hand-revert i ## Checklist -- [ ] Re-run `ganda repo audit` and refresh this list if a row changed -- [ ] `ganda repo audit --fix` for the fixable rows, excluding a self-package pin (see `global-usings-analyzer`) -- [ ] Commit the `--fix` diff (`chore(audit): …`) -- [ ] `charset = utf-8` under `[*]`, quotes removed -- [ ] `global-usings-analyzer.severity = off` with the reason comment above -- [ ] Split `markdown-docs-generator.cs` under the 500 line cap -- [ ] Nuru and DevCli pins match the audit's latest; `dev build` passes -- [ ] `pull_request.branches` includes `feature/**` -- [ ] `ganda repo audit` exits 0 with no Failed row -- [ ] `## Results` and `### How to validate` -- [ ] Archive task 027 once this kitchen owns the journal ignore (one `.gitignore` edit) -- [ ] Stop after product work and Results. Do not `kanban done`. Do not `gh pr create` +- [x] Re-run `ganda repo audit` and refresh this list if a row changed +- [x] `ganda repo audit --fix` for the fixable rows, excluding a self-package pin (see `global-usings-analyzer`) +- [x] Commit the `--fix` diff (`chore(audit): …`) +- [x] `charset = utf-8` under `[*]`, quotes removed +- [x] `global-usings-analyzer.severity = off` with the reason comment above +- [x] Split `markdown-docs-generator.cs` under the 500 line cap +- [x] Nuru and DevCli pins match the audit's latest; `dev build` passes +- [x] `pull_request.branches` includes `feature/**` +- [x] `ganda repo audit` exits 0 with no Failed row +- [x] `## Results` and `### How to validate` +- [x] Archive task 027 once this kitchen owns the journal ignore (one `.gitignore` edit) +- [x] Stop after product work and Results. Do not `kanban done`. Do not `gh pr create` ## Notes - Predecessor: task **024** brought the audit clean on Nuru / DevCli **3.0.0-beta.76**. This task is the next drift. -- Task **027** (`routine-journals-gitignore` only) is still `to-do`. Its requirements are the journal row above. Archive 027 when this task is claimed so two kitchens do not edit `.gitignore`. +- Task **027** (`routine-journals-gitignore` only) was archived from origin-home on 2026-11-08 (`09a725d land kanban 027`) after this branch committed the journal ignore. Archive refuses on the 030 branch because that branch is not 027's task branch. The to-do copy remains on this branch; master owns the archived kitchen, and this branch did not edit that file. - Task **029** adds `UseTelemetry` to `tools/dev-cli/dev.cs` against the Nuru 3.x generator. Do not run 029 in parallel. This task owns the 4.x pin. 029 should land on top of that pin and re-check the `UseTelemetry` call against 4.x. - Claim on-create (2026-11-08) failed `dev` self-install: `MSB3030` could not copy `dev.xml` from the runfile publish output. `bin/dev` may be missing in a fresh worktree until self-install works on the new DevCli pin. The claim still stands. - Policy: `ganda repo audit --fix` output is the baseline. Commit it. The only downgrade allowed here is `global-usings-analyzer`, because this repo is the package the check wants consumers to reference. +- Live re-read before `--fix` (this worktree, 2026-11-08): **Passed 27, Failed 10, Skipped 3**. `kanban-porcelain-gitignore` and `routine-journals-gitignore` already passed from the uncommitted gitignore lines. Nuru latest was still **4.0.0-beta.11**. `--fix` also pinned `TimeWarp.Amuru` and `TimeWarp.Amuru.Tools` to **3.0.0-beta.1** and `TimeWarp.Terminal` to **1.0.2**. Amuru 3 is net11.0 only, so the dev CLI and the git hooks target net11.0, and CI installs `11.0.100-rc.3.26522.148` beside `10.0.x`. ### How to validate @@ -95,7 +112,48 @@ wc -l source/timewarp-source-generators/markdown-docs-generator.cs # Expect: the split files are each <= 500 lines; the old monolith is gone or under the cap ``` +## Results + +`ganda repo audit` exits 0 with no Failed row (**Passed 36, Failed 0, Skipped 3**). `global-usings-analyzer` is off under `[ganda.audit]` with the reason comment, and there is no root `PackageReference` to `TimeWarp.SourceGenerators`. + +`--fix` is committed as `0eac4eb` (`chore(audit): apply ganda repo audit --fix`). That commit keeps the Amuru 3 and Terminal bumps. Hand fixes are `a936c58`. + +Nuru and DevCli are **4.0.0-beta.11**. Nuru 4 moved `ICommand`, `ICommandHandler`, and `Unit` to `TimeWarp.Mediator`, and handlers return `Task`. The dev CLI compiles on net11.0. `dotnet run --file tools/dev-cli/dev.cs -- --help` lists `release`. `check-version` prints `Packages checked: TimeWarp.SourceGenerators` and exits 1 because `1.0.0-beta.11` is already on NuGet. That exit is the release guard, not a missed package. + +`markdown-docs-generator.cs` is 136 lines. Markdown parsing lives in `markdown-docs-generator.markdown-to-xml.cs` (409 lines). `./bin/dev build` succeeded with 0 warnings. `./bin/dev test` passed the test console and 11/11 analyzer tests. The Nuru 4 dev CLI `build` command also succeeded. + +Task 027 is archived on master (`09a725d`). This walk did not run `ganda kanban done` or `gh pr create`. + +CI installs the net11 RC next to net10 so `dotnet run --file tools/dev-cli/dev.cs` can restore Amuru 3. .NET 11 was still `11.0.0-rc.3` on 2026-11-08. + +### How to validate + +**Smoke** + +```bash +ganda repo audit +grep -E 'TimeWarp\.Nuru' Directory.Packages.props +./bin/dev --help | grep release +./bin/dev check-version +./bin/dev build +./bin/dev test +grep -n 'charset' .editorconfig +grep -n 'PackageReference Include="TimeWarp.SourceGenerators"' Directory.Build.props Directory.Packages.props || true +wc -l source/timewarp-source-generators/markdown-docs-generator.cs source/timewarp-source-generators/markdown-docs-generator.markdown-to-xml.cs +``` + +**Expect** + +- `ganda repo audit` exits 0 and the table has no Failed row. +- `TimeWarp.Nuru` and `TimeWarp.Nuru.DevCli` are `4.0.0-beta.11`. +- Help text includes `release`. `check-version` lists `TimeWarp.SourceGenerators`. +- `./bin/dev build` succeeds with 0 errors. `./bin/dev test` passes the analyzer cases. +- `.editorconfig` has `charset = utf-8` under `[*]` and no quoted charset. +- No `PackageReference` to `TimeWarp.SourceGenerators`. +- Both markdown generator files are at most 500 lines. + ## Session - Created: grok `01a1bbd2-4185-7452-8535-389eb11a4f8f` (2026-11-08) - Trigger: Steve asked to create a task to fix `ganda repo audit` on timewarp-source-generators +- Implementation: grok `01a1bbe3-2e77-79f0-ae81-48feae04ec9c` (2026-11-08) From 36a03b1d3a70208d8aafcc4b62dfa5a443670f5c Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 8 Nov 2026 23:21:32 +0700 Subject: [PATCH 5/6] chore(kanban): land task 030 --- kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md | 1 + 1 file changed, 1 insertion(+) diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md index 55437a4..dc9b75e 100644 --- a/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md @@ -157,3 +157,4 @@ wc -l source/timewarp-source-generators/markdown-docs-generator.cs source/timewa - Created: grok `01a1bbd2-4185-7452-8535-389eb11a4f8f` (2026-11-08) - Trigger: Steve asked to create a task to fix `ganda repo audit` on timewarp-source-generators - Implementation: grok `01a1bbe3-2e77-79f0-ae81-48feae04ec9c` (2026-11-08) +- Implement oracle: implemented by implementer-grok (grok, vendor xai, model grok-4.7, response model grok-4.7-build), session 01a1bbe3-2e77-79f0-ae81-48feae04ec9c, max-turns 500, Done — 2026-11-08T16:21:32Z From 2c6af7fe1969bf93cb574c74df8ad4d7da2b2453 Mon Sep 17 00:00:00 2001 From: "Steven T. Cramer" Date: Sun, 8 Nov 2026 23:24:11 +0700 Subject: [PATCH 6/6] chore(kanban): land task 030 --- .../review/disposition.md | 18 ++++++++++ .../review/review-framework.md | 28 +++++++++++++++ .../review/round-1/general.md | 34 +++++++++++++++++++ .../review/round-1/merged.md | 19 +++++++++++ .../030-bring-ganda-repo-audit-clean/task.md | 14 ++++++++ 5 files changed, 113 insertions(+) create mode 100644 kanban/in-progress/030-bring-ganda-repo-audit-clean/review/disposition.md create mode 100644 kanban/in-progress/030-bring-ganda-repo-audit-clean/review/review-framework.md create mode 100644 kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/general.md create mode 100644 kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/merged.md diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/disposition.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/disposition.md new file mode 100644 index 0000000..9d93f20 --- /dev/null +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/disposition.md @@ -0,0 +1,18 @@ +# Disposition — task 030 + +**Date:** 2026-11-08 +**Outcome:** clean +**Rounds:** 1 +**Final open count:** 0 + +## Summary + +The round-1 general review (effort 3, general axis) raised no findings. The live gates passed in the claim worktree. `ganda repo audit` exits 0 with Passed 36, Failed 0, Skipped 3. `./bin/dev build` reported 0 warnings and 0 errors. `./bin/dev test` passed 11/11. `check-version` sees `TimeWarp.SourceGenerators`. Nuru and DevCli are at 4.0.0-beta.11. The markdown generator split is a pure move under the 500-line cap. `global-usings-analyzer` is off with the required reason comment, and there is no self-package reference. + +## Exception log (if accepted-exceptions) + +None. + +## Escalations + +- None. Follow-up after .NET 11 GA: change the CI SDK pin from `11.0.100-rc.3.26522.148` to `11.0.x`. diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/review-framework.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/review-framework.md new file mode 100644 index 0000000..cbefe88 --- /dev/null +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/review-framework.md @@ -0,0 +1,28 @@ +# Review framework + +## Budget (by-diff) + +- Lines changed: 1590 +- Effort: 3 +- TCB hits: none +- Roster axes: general +- Turn cap: 200 (--max-turns; cursor uncapped) + +## Scope + +**Date:** 2026-11-08 +**Host task:** kanban/in-progress/030-bring-ganda-repo-audit-clean/ +**Diff scope:** branch `task/030-bring-ganda-repo-audit-clean` (HEAD `36a03b1`) vs merge-base `c225708` with `origin/master` +**Plan / brief:** task.md `## Goal (implementer restatement)` and `## Requirements`. Make `ganda repo audit` pass, move Nuru/DevCli to 4.0.0-beta.11, split `markdown-docs-generator.cs` under 500 lines, and turn `global-usings-analyzer` off with a reason comment +**Effort:** 3 (by-diff budget); roster axis general only +**Reviewer roster:** general +**Session IDs:** review oracle Claude (claude-opus-5-5), ganda task work headless + +## Ground rules + +- Reviewers are read-only on product code; they write only under `review/round-N/` +- Severity: bug | suggestion | nit — Status starts as open +- Do not invent issues to fill space; zero issues is a valid outcome +- Address the diff and surrounding call sites; re-verify falsifiable claims against the repo +- Prior rounds are immutable; new work goes in `round-(N+1)/` +- `ganda repo audit --fix` output (hook scaffolds, glossary template, package pins) is policy baseline, not a finding diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/general.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/general.md new file mode 100644 index 0000000..72dae53 --- /dev/null +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/general.md @@ -0,0 +1,34 @@ +# Round 1 — general +**Date:** 2026-11-08 +**Scope reviewed:** `task/030-bring-ganda-repo-audit-clean` vs merge-base `c225708` (25 files, +919/−671) + +## Summary + +The branch commits the `ganda repo audit --fix` output (`0eac4eb`) and then hand-fixes the rest (`a936c58`). The hand fixes are: unquoted `charset = utf-8` under `[*]`, `global-usings-analyzer.severity = off` with the reason comment the task asked for, a Nuru 4 port of the dev CLI, net11 retargets for the Amuru 3 pin, and the markdown generator split. Risk is low. The generator split is a pure move. A whitespace-insensitive, sorted line comparison of the old file against the two new files shows only the new `partial` keyword, the Purpose/Design regions, and the second file's namespace and class wrapper. + +## Verification (live, this worktree) + +- `ganda repo audit`: exit 0, **Passed 36 | Failed 0 | Skipped 3**, "Repository passes all audit checks." +- `./bin/dev build`: Build succeeded, 0 warnings, 0 errors. +- `./bin/dev test`: 11/11 analyzer cases passed. +- `./bin/dev --help` lists `release` and `check-version`. +- `./bin/dev check-version` prints `Packages checked: TimeWarp.SourceGenerators`. It fails on the already-published `1.0.0-beta.11`, which is the release guard working as intended. +- `Directory.Packages.props` pins `TimeWarp.Nuru` and `TimeWarp.Nuru.DevCli` at `4.0.0-beta.11`. The audit's `nuru` row passes. +- `.editorconfig` has `charset = utf-8` at line 26 under `[*]`, and the quoted glob-scoped line is gone. +- No `PackageReference Include="TimeWarp.SourceGenerators"` in root `Directory.Build.props` or `Directory.Packages.props`. The `TW0007.filename` line is kept. +- `markdown-docs-generator.cs` is 136 lines and `markdown-docs-generator.markdown-to-xml.cs` is 409 lines. No `part-N` naming. The audit's `source-file-line-cap` passes. +- `workflow.yml` `pull_request.branches` is `[master, 'feature/**']`. +- `git ls-files '*.journal.json' .vscode/settings.json .memsearch.toml` is empty. The working copy `.vscode/settings.json` sets `peacock.color`. + +## Goal restatement vs task + +The restatement matches the task's Description and Requirements. It includes the 027 archive step and the stop before `kanban done` / `gh pr create`. No misread. + +## Issues + +None. The `#pragma warning disable RS0030, CA1849` blocks and `RunAnalyzers=false` in `.githooks/*.cs` come from the audit `memsearch-scaffold` baseline that `--fix` wrote. They are not suppressions added to make a check pass, so they are not findings under the audit-fix policy. + +## Notes (not findings) + +- CI pins the exact SDK `11.0.100-rc.3.26522.148` next to `10.0.x`. When .NET 11 reaches GA, switch that line to `11.0.x`. This is a follow-up after GA, not a defect now. +- Task 029 (`UseTelemetry`) must land on top of the Nuru 4 pin, as task.md Notes already say. diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/merged.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/merged.md new file mode 100644 index 0000000..4ea9cc3 --- /dev/null +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/review/round-1/merged.md @@ -0,0 +1,19 @@ +# Round 1 — merged findings +**Date:** 2026-11-08 +**Sources:** general + +## Counts + +| Severity | open | fixed | wontfix | +|----------|------|-------|---------| +| bug | 0 | 0 | 0 | +| suggestion | 0 | 0 | 0 | +| nit | 0 | 0 | 0 | + +## Issues + +None raised. See `general.md` for the live gate checks (audit exit 0, build, 11/11 tests, check-version, pin and line-cap checks). + +## Duplicates / conflicts + +- None (single reviewer). diff --git a/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md index dc9b75e..22b1a7d 100644 --- a/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md @@ -77,6 +77,10 @@ These rows reported Fixable YES. Commit the `--fix` output. Do not hand-revert i - [x] Archive task 027 once this kitchen owns the journal ignore (one `.gitignore` edit) - [x] Stop after product work and Results. Do not `kanban done`. Do not `gh pr create` +## Review + +- Round 1: Done + ## Notes - Predecessor: task **024** brought the audit clean on Nuru / DevCli **3.0.0-beta.76**. This task is the next drift. @@ -126,6 +130,14 @@ Task 027 is archived on master (`09a725d`). This walk did not run `ganda kanban CI installs the net11 RC next to net10 so `dotnet run --file tools/dev-cli/dev.cs` can restore Amuru 3. .NET 11 was still `11.0.0-rc.3` on 2026-11-08. +### Review disposition + +- **Disposition:** clean. Rounds: 1. Effort 3, roster: general. +- **Final counts:** bug 0, suggestion 0, nit 0 (0 open, 0 fixed, 0 wontfix). +- **Live checks by the reviewer:** `ganda repo audit` exit 0 (Passed 36, Failed 0, Skipped 3). `./bin/dev build` reported 0 warnings and 0 errors. `./bin/dev test` passed 11/11. `check-version` lists `TimeWarp.SourceGenerators`. The generator split is confirmed as a pure move. +- **Follow-up (not a finding):** after .NET 11 GA, change the CI SDK line `11.0.100-rc.3.26522.148` to `11.0.x`. +- **Paths:** `review/review-framework.md`, `review/round-1/general.md`, `review/round-1/merged.md`, `review/disposition.md`. + ### How to validate **Smoke** @@ -158,3 +170,5 @@ wc -l source/timewarp-source-generators/markdown-docs-generator.cs source/timewa - Trigger: Steve asked to create a task to fix `ganda repo audit` on timewarp-source-generators - Implementation: grok `01a1bbe3-2e77-79f0-ae81-48feae04ec9c` (2026-11-08) - Implement oracle: implemented by implementer-grok (grok, vendor xai, model grok-4.7, response model grok-4.7-build), session 01a1bbe3-2e77-79f0-ae81-48feae04ec9c, max-turns 500, Done — 2026-11-08T16:21:32Z +- Review oracle: tw-implementation-review round 1, Claude (claude-opus-5-5), ganda task work headless, effort 3, roster general, turn cap 200. Disposition clean — 2026-11-08 +- Review oracle: review by reviewer-claude (claude, vendor anthropic, model claude-opus-5-5, response model claude-opus-5-5), session 96240dfd-27eb-43b9-ac1e-aea0e6bdce7d, max-turns 200, artifacts general.md, merged.md, disposition.md — 2026-11-08T16:24:11Z