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/.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..69077b3 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/**' @@ -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/.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. | 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 new file mode 100644 index 0000000..22b1a7d --- /dev/null +++ b/kanban/in-progress/030-bring-ganda-repo-audit-clean/task.md @@ -0,0 +1,174 @@ +# 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. +Snapshot from 2026-11-08 (`origin/master`, clean): **Passed 25, Failed 12, Skipped 3**, exit 1. +Ten rows are Error. Two are Warning (`memsearch-scaffold`, `vscode-window-icon`). Both count as Failed. + +Bring the repo to a passing audit. Run `ganda repo audit --fix` for the rows it can fix, commit that result, then hand-fix the rest. A major package bump from `--fix` is the baseline (do not revert it). Downgrade a check under `[ganda.audit]` only when the check does not apply, with a comment that names the reason and this task id. + +Re-read `ganda repo audit` at the start. Pins and "latest" versions move. The list below is the 2026-11-08 snapshot. + +## Requirements + +- `ganda repo audit` exits 0. The results table has no Failed row. +- `./bin/dev build` succeeds after the Nuru / DevCli pin move (0 errors). +- Every `[ganda.audit]` downgrade has a comment with the reason and task 030. +- Do not implement on `master`. Work stays in the claimed task worktree. + +### Fix with `ganda repo audit --fix` + +These rows reported Fixable YES. Commit the `--fix` output. Do not hand-revert it. + +- `glossary` — `glossary.md` is missing. +- `kanban-porcelain-gitignore` — root `.gitignore` missing porcelain lines (`*.journal.json`, `*.next.md`, `*.progress.log`, `**/review/round-*/base.sha`, `**/review/round-*/closed`, `oracle-*.log`, `task-work.progress.log`, `mission.heartbeat`). +- `required-gitignore-entries` — missing `.local/`, `bin/dev`, `bin/dev.dbg`, `bin/dev.exe`. +- `routine-journals-gitignore` — same journal globs, plus `stacked-task-set*.resume.json`. One `*.journal.json` line covers the journal basenames. Do not commit journal contents. `git ls-files '*.journal.json'` must be empty. +- `memsearch-scaffold` (Warning) — leftover `.memsearch.toml`; outdated `.githooks/post-commit.cs`, `post-merge.cs`, `post-checkout`, `pre-commit`, `pre-push`. +- `vscode-settings-gitignore` — `.vscode/settings.json` is tracked. `--fix` runs `git rm --cached` and adds the ignore. Keep the working copy. Do not commit `settings.json`. +- `vscode-window-icon` (Warning) — `.vscode/settings.json` does not set `peacock.color`. Set it in the working copy after the file is untracked. +- `workflow-file` — `.github/workflows/workflow.yml` `pull_request.branches` is `[master]`. It must include `feature/**`. +- `nuru` — `Directory.Packages.props` pins `TimeWarp.Nuru` and `TimeWarp.Nuru.DevCli` at `3.0.0-beta.76`. Audit latest on this date: **4.0.0-beta.11** (nuget.org). Bump **both** packages to the version the audit reports when the work starts. This is a 3.x → 4.x move. Update `tools/dev-cli` if the 4.x API no longer compiles. `dev --help` must still show `release`, and `dev check-version` must still see `TimeWarp.SourceGenerators`. + +### Hand fixes (`--fix` will not do these) + +- `editorconfig-charset` (Error, not fixable) — `charset` is `"utf-8"` under the `[*.{cs,...}]` glob. The check wants `charset = utf-8` (no quotes) under `[*]`. +- `source-file-line-cap` (Error, not fixable) — `source/timewarp-source-generators/markdown-docs-generator.cs` is **532** lines. Cap is **500**. Split by responsibility. No partial-class `part-N` names. Behavior stays; analyzer tests still pass. +- `global-usings-analyzer` (Error, fixable, **do not take the default fix**). The check looks for the string `TimeWarp.SourceGenerators` in root `Directory.Packages.props` and root `Directory.Build.props`. `--fix` would pin the NuGet package and add a root `PackageReference`. This repo **is** that package. A root reference would make the generator project consume the published package. Set: + + ```ini + # 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 + ``` + + Leave the existing `dotnet_diagnostic.TW0007.filename = global-usings.cs` line. Do not add a `PackageReference` to `TimeWarp.SourceGenerators`. + +## Checklist + +- [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` + +## 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. +- 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 + +```bash +ganda repo audit +# Expect: exit 0, no Failed row + +grep -E 'TimeWarp\.Nuru' Directory.Packages.props +# Expect: Nuru and DevCli at the version the audit called latest + +./bin/dev --help | grep release +./bin/dev check-version +./bin/dev build +# Expect: build succeeds + +# charset +grep -n 'charset' .editorconfig +# Expect: charset = utf-8 under [*] + +# no self-package reference +grep -n 'PackageReference Include="TimeWarp.SourceGenerators"' Directory.Build.props Directory.Packages.props +# Expect: no PackageReference (PackageId in source/Directory.Build.props is the product id, and is fine) + +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. + +### 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** + +```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) +- 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 diff --git a/kanban/to-do/030-bring-ganda-repo-audit-clean/task.md b/kanban/to-do/030-bring-ganda-repo-audit-clean/task.md deleted file mode 100644 index 320c19f..0000000 --- a/kanban/to-do/030-bring-ganda-repo-audit-clean/task.md +++ /dev/null @@ -1,101 +0,0 @@ -# Bring ganda repo audit clean - -## Description - -`ganda repo audit` on `timewarp-source-generators` master fails. -Snapshot from 2026-11-08 (`origin/master`, clean): **Passed 25, Failed 12, Skipped 3**, exit 1. -Ten rows are Error. Two are Warning (`memsearch-scaffold`, `vscode-window-icon`). Both count as Failed. - -Bring the repo to a passing audit. Run `ganda repo audit --fix` for the rows it can fix, commit that result, then hand-fix the rest. A major package bump from `--fix` is the baseline (do not revert it). Downgrade a check under `[ganda.audit]` only when the check does not apply, with a comment that names the reason and this task id. - -Re-read `ganda repo audit` at the start. Pins and "latest" versions move. The list below is the 2026-11-08 snapshot. - -## Requirements - -- `ganda repo audit` exits 0. The results table has no Failed row. -- `./bin/dev build` succeeds after the Nuru / DevCli pin move (0 errors). -- Every `[ganda.audit]` downgrade has a comment with the reason and task 030. -- Do not implement on `master`. Work stays in the claimed task worktree. - -### Fix with `ganda repo audit --fix` - -These rows reported Fixable YES. Commit the `--fix` output. Do not hand-revert it. - -- `glossary` — `glossary.md` is missing. -- `kanban-porcelain-gitignore` — root `.gitignore` missing porcelain lines (`*.journal.json`, `*.next.md`, `*.progress.log`, `**/review/round-*/base.sha`, `**/review/round-*/closed`, `oracle-*.log`, `task-work.progress.log`, `mission.heartbeat`). -- `required-gitignore-entries` — missing `.local/`, `bin/dev`, `bin/dev.dbg`, `bin/dev.exe`. -- `routine-journals-gitignore` — same journal globs, plus `stacked-task-set*.resume.json`. One `*.journal.json` line covers the journal basenames. Do not commit journal contents. `git ls-files '*.journal.json'` must be empty. -- `memsearch-scaffold` (Warning) — leftover `.memsearch.toml`; outdated `.githooks/post-commit.cs`, `post-merge.cs`, `post-checkout`, `pre-commit`, `pre-push`. -- `vscode-settings-gitignore` — `.vscode/settings.json` is tracked. `--fix` runs `git rm --cached` and adds the ignore. Keep the working copy. Do not commit `settings.json`. -- `vscode-window-icon` (Warning) — `.vscode/settings.json` does not set `peacock.color`. Set it in the working copy after the file is untracked. -- `workflow-file` — `.github/workflows/workflow.yml` `pull_request.branches` is `[master]`. It must include `feature/**`. -- `nuru` — `Directory.Packages.props` pins `TimeWarp.Nuru` and `TimeWarp.Nuru.DevCli` at `3.0.0-beta.76`. Audit latest on this date: **4.0.0-beta.11** (nuget.org). Bump **both** packages to the version the audit reports when the work starts. This is a 3.x → 4.x move. Update `tools/dev-cli` if the 4.x API no longer compiles. `dev --help` must still show `release`, and `dev check-version` must still see `TimeWarp.SourceGenerators`. - -### Hand fixes (`--fix` will not do these) - -- `editorconfig-charset` (Error, not fixable) — `charset` is `"utf-8"` under the `[*.{cs,...}]` glob. The check wants `charset = utf-8` (no quotes) under `[*]`. -- `source-file-line-cap` (Error, not fixable) — `source/timewarp-source-generators/markdown-docs-generator.cs` is **532** lines. Cap is **500**. Split by responsibility. No partial-class `part-N` names. Behavior stays; analyzer tests still pass. -- `global-usings-analyzer` (Error, fixable, **do not take the default fix**). The check looks for the string `TimeWarp.SourceGenerators` in root `Directory.Packages.props` and root `Directory.Build.props`. `--fix` would pin the NuGet package and add a root `PackageReference`. This repo **is** that package. A root reference would make the generator project consume the published package. Set: - - ```ini - # 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 - ``` - - Leave the existing `dotnet_diagnostic.TW0007.filename = global-usings.cs` line. Do not add a `PackageReference` to `TimeWarp.SourceGenerators`. - -## 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` - -## 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 **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. - -### How to validate - -```bash -ganda repo audit -# Expect: exit 0, no Failed row - -grep -E 'TimeWarp\.Nuru' Directory.Packages.props -# Expect: Nuru and DevCli at the version the audit called latest - -./bin/dev --help | grep release -./bin/dev check-version -./bin/dev build -# Expect: build succeeds - -# charset -grep -n 'charset' .editorconfig -# Expect: charset = utf-8 under [*] - -# no self-package reference -grep -n 'PackageReference Include="TimeWarp.SourceGenerators"' Directory.Build.props Directory.Packages.props -# Expect: no PackageReference (PackageId in source/Directory.Build.props is the product id, and is fine) - -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 -``` - -## 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 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;