diff --git a/.editorconfig b/.editorconfig index 7c84cac..557c197 100644 --- a/.editorconfig +++ b/.editorconfig @@ -274,8 +274,8 @@ dotnet_naming_style.local_function_style.capitalization = pascal_case # dotnet_diagnostic options dotnet_diagnostic.CS1591.severity = none -dotnet_diagnostic.TWA001.severity = none # Set to warning/error to enforce kebab-case file naming -dotnet_diagnostic.TWA001.excluded_files = *.g.cs;*.Generated.cs;*.generated.cs;*.designer.cs;*.Designer.cs;*.razor.cs;Directory.Build.props;Directory.Build.targets;Directory.Packages.props;AssemblyInfo.cs;*.AssemblyAttributes.cs;AnalyzerReleases.Shipped.md;AnalyzerReleases.Unshipped.md +dotnet_diagnostic.TW0001.severity = none # Set to warning/error to enforce kebab-case file naming +dotnet_diagnostic.TW0001.excluded_files = *.g.cs;*.Generated.cs;*.generated.cs;*.designer.cs;*.Designer.cs;*.razor.cs;Directory.Build.props;Directory.Build.targets;Directory.Packages.props;AssemblyInfo.cs;*.AssemblyAttributes.cs;AnalyzerReleases.Shipped.md;AnalyzerReleases.Unshipped.md #### Analyzer settings #### dotnet_code_quality.null_check_validation_methods = NotNull diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..234188d --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +PATH_add bin diff --git a/.githooks/post-commit b/.githooks/post-commit new file mode 120000 index 0000000..50e32c5 --- /dev/null +++ b/.githooks/post-commit @@ -0,0 +1 @@ +post-commit.cs \ No newline at end of file diff --git a/.githooks/post-commit.cs b/.githooks/post-commit.cs new file mode 100755 index 0000000..a6f0920 --- /dev/null +++ b/.githooks/post-commit.cs @@ -0,0 +1,19 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:package TimeWarp.Amuru.Tools +#:property NoWarn=CA2007 + +using TimeWarp.Amuru; + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +await Shell.Builder("ganda") + .WithArguments("memsearch", "index-repo", "--background") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +return 0; \ No newline at end of file diff --git a/.githooks/post-merge b/.githooks/post-merge new file mode 120000 index 0000000..6031064 --- /dev/null +++ b/.githooks/post-merge @@ -0,0 +1 @@ +post-merge.cs \ No newline at end of file diff --git a/.githooks/post-merge.cs b/.githooks/post-merge.cs new file mode 100755 index 0000000..a6f0920 --- /dev/null +++ b/.githooks/post-merge.cs @@ -0,0 +1,19 @@ +#!/usr/bin/env -S dotnet -- +#:package TimeWarp.Amuru +#:package TimeWarp.Amuru.Tools +#:property NoWarn=CA2007 + +using TimeWarp.Amuru; + +string? root = Git.FindRoot(); +if (root is null) +{ + return 0; +} + +await Shell.Builder("ganda") + .WithArguments("memsearch", "index-repo", "--background") + .WithWorkingDirectory(root) + .WithNoValidation() + .RunAsync(); +return 0; \ No newline at end of file diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index aa9e055..0000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: CI/CD Pipeline - -on: - push: - branches: - - master - paths: - - 'source/**' - - 'tests/**' - - '.github/workflows/**' - - 'Directory.Build.props' - - '*.props' - - '*.targets' - pull_request: - branches: - - master - paths: - - 'source/**' - - 'tests/**' - - '.github/workflows/**' - - 'Directory.Build.props' - - '*.props' - - '*.targets' - release: - types: [published] # Triggered when a release is published via GitHub Releases UI or gh CLI - workflow_dispatch: - -env: - DOTNET_NOLOGO: true - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - shell: pwsh - steps: - - name: πŸŽ‰ Event Trigger - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." - - - name: πŸ“₯ Check out repository code - uses: actions/checkout@v4 - - - name: πŸ“¦ Cache NuGet packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: πŸ› οΈ Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: πŸ“ Create artifacts directory - run: mkdir -p artifacts/packages - - - name: πŸ”¨ Build - run: | - dotnet build source/timewarp-source-generators/timewarp-source-generators.csproj --configuration Release - - # TODO: Enable tests when ready - # - name: πŸ§ͺ Run Tests - # run: | - # cd tests/timewarp-source-generators-tests/ - # dotnet tool restore - # dotnet restore - # dotnet fixie --configuration Release - - - name: πŸ” Check if version already published (Releases only) - if: github.event_name == 'release' - run: | - $version = (Select-Xml -Path "source/Directory.Build.props" -XPath "//Version/text()").Node.Value - Write-Host "Checking if TimeWarp.SourceGenerators $version is already published on NuGet.org..." - - $searchResult = dotnet package search TimeWarp.SourceGenerators --exact-match --prerelease --source https://api.nuget.org/v3/index.json - if ($searchResult -match "TimeWarp\.SourceGenerators.*$version") { - Write-Host "⚠️ WARNING: TimeWarp.SourceGenerators $version is already published to NuGet.org" - Write-Host "❌ This version cannot be republished. Please increment the version in source/Directory.Build.props" - exit 1 - } else { - Write-Host "βœ… TimeWarp.SourceGenerators $version is not yet published on NuGet.org" - } - - - name: πŸš€ Publish to NuGet.org (Releases only) - if: github.event_name == 'release' - run: | - dotnet nuget push artifacts/packages/TimeWarp.SourceGenerators.*.nupkg ` - --api-key ${{ secrets.PUBLISH_TO_NUGET_ORG }} ` - --source https://api.nuget.org/v3/index.json ` - --skip-duplicate - - - name: πŸ“€ Upload Artifacts - if: github.event_name == 'push' || github.event_name == 'release' - uses: actions/upload-artifact@v4 - with: - name: Packages-${{ github.run_number }} - path: artifacts/packages/*.nupkg - - - name: βœ… Job Status - run: echo "This job's status is ${{ job.status }}." \ No newline at end of file diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..0a715c9 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,64 @@ +name: CI/CD + +on: + push: + branches: [master] + paths: + - 'source/**' + - 'tests/**' + - 'tools/**' + - 'msbuild/**' + - '.github/workflows/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'source/Directory.Build.props' + - 'nuget.config' + pull_request: + branches: [master] + paths: + - 'source/**' + - 'tests/**' + - 'tools/**' + - 'msbuild/**' + - '.github/workflows/**' + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'source/Directory.Build.props' + - 'nuget.config' + release: + types: [published] + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: NuGet login (OIDC Trusted Publishing) + if: github.event_name == 'release' + id: nuget-login + uses: nuget/login@v1 + with: + user: TimeWarp.Enterprises + - name: Run CI Pipeline + run: | + if [ "${{ github.event_name }}" == "release" ]; then + dotnet run --file tools/dev-cli/dev.cs -- workflow --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" + else + dotnet run --file tools/dev-cli/dev.cs -- workflow + fi + - name: Upload Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: Packages-${{ github.run_number }} + path: artifacts/packages/*.nupkg + if-no-files-found: ignore diff --git a/.memsearch.toml b/.memsearch.toml new file mode 100644 index 0000000..00bcc0e --- /dev/null +++ b/.memsearch.toml @@ -0,0 +1,50 @@ +# 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 index e667d77..770dc45 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,5 +24,7 @@ "buildtransitive", "contentfiles", "nugets" - ] -} \ No newline at end of file + ], + "window.title": "timewarp-source-generators Β· ${rootName}${separator}${activeEditorShort}", + "timewarp.blurImagePath": "assets/timewarp-source-generators-avatar.svg" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f82f57f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "ganda: window icon", + "type": "shell", + "command": "ganda repo avatar window", + "runOptions": { + "runOn": "folderOpen" + }, + "presentation": { + "reveal": "silent", + "echo": false + }, + "problemMatcher": [] + } + ] +} diff --git a/BannedSymbols.txt b/BannedSymbols.txt new file mode 100644 index 0000000..94da1d3 --- /dev/null +++ b/BannedSymbols.txt @@ -0,0 +1,3 @@ + +T:System.Console;Prefer injecting ITerminal. TimeWarp.Terminal.Terminal static class is available for migration. +T:System.Diagnostics.ProcessStartInfo;Use TimeWarp.Amuru Shell.Builder instead. See the 'amuru' skill for usage patterns. diff --git a/Directory.Build.props b/Directory.Build.props index fde08c3..f200957 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,6 @@ + + @@ -24,7 +26,7 @@ - net9.0 + net10.0 enable enable latest @@ -57,13 +59,13 @@ - + - + - + @@ -80,6 +82,12 @@ all + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index d7363a6..3be507b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,20 +1,27 @@ + - - - - - - - - - + + true + + + - - - - - - + + + + + + + + + + + + + + + + diff --git a/documentation/developer/conceptual/analyzer-release-tracking.md b/documentation/developer/conceptual/analyzer-release-tracking.md index da4bc3a..e94ad94 100644 --- a/documentation/developer/conceptual/analyzer-release-tracking.md +++ b/documentation/developer/conceptual/analyzer-release-tracking.md @@ -71,9 +71,9 @@ AnalyzerReleases.Unshipped.md: ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- -TWG001 | SourceGenerator | Info | MarkdownDocsGenerator -TWA001 | Naming | Info | FileNameRuleAnalyzer, disabled by default -TWA002 | Documentation | Info | XmlDocsToMarkdownAnalyzer +TW0001 | Naming | Info | FileNameRuleAnalyzer, disabled by default +TW0002 | Documentation | Info | XmlDocsToMarkdownAnalyzer +TW0003 | SourceGenerator | Info | MarkdownDocsGenerator AnalyzerReleases.Shipped.md: ## Release 1.0 diff --git a/documentation/developer/how-to-guides/configure-file-name-analyzer.md b/documentation/developer/how-to-guides/configure-file-name-analyzer.md index dff5f10..a717bc9 100644 --- a/documentation/developer/how-to-guides/configure-file-name-analyzer.md +++ b/documentation/developer/how-to-guides/configure-file-name-analyzer.md @@ -1,70 +1,70 @@ -# How to Configure the File Name Analyzer - -This guide walks you through configuring the TW0003 file name analyzer to enforce kebab-case naming in your project. - -## Prerequisites - -- TimeWarp.SourceGenerators package referenced in your project -- An `.editorconfig` file in your project root - -## Step 1: Enable the Analyzer - -Add the following to your `.editorconfig`: - -```ini -[*.cs] -dotnet_diagnostic.TW0003.severity = warning -``` - -Severity options: -- `none` - Disabled (default) -- `suggestion` - Shows as info in IDE -- `warning` - Shows as warning -- `error` - Fails the build - -## Step 2: Configure Exceptions - -Add patterns for files that should be excluded: - -```ini -dotnet_diagnostic.TW0003.excluded_files = *.g.cs;*.Generated.cs;Program.cs -``` - -## Step 3: Test the Configuration - -1. Build your project -2. Check for TW0003 warnings on non-kebab-case files -3. Verify exceptions are working - -## Step 4: Gradual Adoption - -For existing projects, start with specific folders: - -```ini -# Global setting - suggestion only -[*.cs] -dotnet_diagnostic.TW0003.severity = suggestion - -# Strict for new features -[Features/**.cs] -dotnet_diagnostic.TW0003.severity = error -``` - -## Step 5: Fix Violations - -Rename files to kebab-case: -- `UserService.cs` β†’ `user-service.cs` -- `IDataProcessor.cs` β†’ `i-data-processor.cs` - -## Troubleshooting - -### Too Many Violations -Add more exception patterns or reduce severity to `suggestion`. - -### Exceptions Not Working -- Check pattern syntax (use `*` for wildcards) -- Verify `.editorconfig` is in the correct location - -### Analyzer Not Running -- Clean and rebuild the project -- Check that the package is properly referenced \ No newline at end of file +# How to Configure the File Name Analyzer + +This guide walks you through configuring the TW0001 file name analyzer to enforce kebab-case naming in your project. + +## Prerequisites + +- TimeWarp.SourceGenerators package referenced in your project +- An `.editorconfig` file in your project root + +## Step 1: Enable the Analyzer + +Add the following to your `.editorconfig`: + +```ini +[*.cs] +dotnet_diagnostic.TW0001.severity = warning +``` + +Severity options: +- `none` - Disabled (default) +- `suggestion` - Shows as info in IDE +- `warning` - Shows as warning +- `error` - Fails the build + +## Step 2: Configure Exceptions + +Add patterns for files that should be excluded: + +```ini +dotnet_diagnostic.TW0001.excluded_files = *.g.cs;*.Generated.cs;Program.cs +``` + +## Step 3: Test the Configuration + +1. Build your project +2. Check for TW0001 warnings on non-kebab-case files +3. Verify exceptions are working + +## Step 4: Gradual Adoption + +For existing projects, start with specific folders: + +```ini +# Global setting - suggestion only +[*.cs] +dotnet_diagnostic.TW0001.severity = suggestion + +# Strict for new features +[Features/**.cs] +dotnet_diagnostic.TW0001.severity = error +``` + +## Step 5: Fix Violations + +Rename files to kebab-case: +- `UserService.cs` β†’ `user-service.cs` +- `IDataProcessor.cs` β†’ `i-data-processor.cs` + +## Troubleshooting + +### Too Many Violations +Add more exception patterns or reduce severity to `suggestion`. + +### Exceptions Not Working +- Check pattern syntax (use `*` for wildcards) +- Verify `.editorconfig` is in the correct location + +### Analyzer Not Running +- Clean and rebuild the project +- Check that the package is properly referenced diff --git a/documentation/developer/reference/analyzers/file-name-rule-analyzer.md b/documentation/developer/reference/analyzers/file-name-rule-analyzer.md index 1c8bb26..0930838 100644 --- a/documentation/developer/reference/analyzers/file-name-rule-analyzer.md +++ b/documentation/developer/reference/analyzers/file-name-rule-analyzer.md @@ -1,128 +1,128 @@ -# FileNameRuleAnalyzer (TWA001) - -## Overview - -The FileNameRuleAnalyzer enforces kebab-case naming conventions for C# source files. This analyzer helps maintain consistent file naming across projects that prefer kebab-case (e.g., `my-component.cs`) over traditional PascalCase. - -## Rule Details - -- **Rule ID**: TWA001 -- **Category**: Naming -- **Default Severity**: Info (disabled by default) -- **Message**: File '{0}' should use kebab-case naming convention (e.g., 'my-file.cs') - -## Kebab-Case Pattern - -Valid kebab-case file names must: -- Start with a lowercase letter -- Use only lowercase letters and numbers -- Separate words with hyphens (-) -- Not contain consecutive hyphens -- End with `.cs` extension - -### Valid Examples -- `user-service.cs` -- `data-processor.cs` -- `my-component.cs` -- `api-client-v2.cs` - -### Invalid Examples -- `UserService.cs` (PascalCase) -- `userService.cs` (camelCase) -- `user_service.cs` (snake_case) -- `user--service.cs` (consecutive hyphens) -- `-user-service.cs` (starts with hyphen) - -## Configuration - -### Enabling the Analyzer - -The analyzer is disabled by default to avoid breaking existing projects. To enable it, add to your `.editorconfig`: - -```ini -# Enable with warning severity -dotnet_diagnostic.TWA001.severity = warning - -# Or as an error to fail builds -dotnet_diagnostic.TWA001.severity = error - -# Or as a suggestion for gentle nudging -dotnet_diagnostic.TWA001.severity = suggestion -``` - -### Exception Patterns - -Configure files that should be excluded from the rule: - -```ini -dotnet_diagnostic.TWA001.excluded_files = *.g.cs;*.Generated.cs;GlobalUsings.cs;Program.cs;Startup.cs -``` - -Default exceptions include: -- Generated files (`*.g.cs`, `*.Generated.cs`, `*.designer.cs`) -- Razor component code-behind files (`*.razor.cs`) - must match their `.razor` file names -- Build files (`Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props`) -- Assembly metadata (`AssemblyInfo.cs`) -- Analyzer tracking files (`AnalyzerReleases.Shipped.md`, `AnalyzerReleases.Unshipped.md`) - -## Implementation Details - -The analyzer is implemented as an `IIncrementalGenerator` for optimal performance: -- Only re-analyzes changed files -- Efficient caching of analysis results -- Minimal impact on build times - -## Use Cases - -This analyzer is useful for: -- Teams transitioning from other languages (e.g., JavaScript/TypeScript) that use kebab-case -- Projects that want to align file naming with URL patterns -- Maintaining consistency with kebab-case component naming in frameworks - -## Examples - -### Project-Wide Configuration - -In your root `.editorconfig`: - -```ini -[*.cs] -# Enable kebab-case file naming for all C# files -dotnet_diagnostic.TWA001.severity = warning - -# But allow exceptions for specific patterns -dotnet_diagnostic.TWA001.excluded_files = *.g.cs;*.Generated.cs;*.designer.cs;Program.cs;Startup.cs;GlobalUsings.cs;AssemblyInfo.cs -``` - -### Gradual Adoption - -For existing projects, start with suggestion severity: - -```ini -[*.cs] -dotnet_diagnostic.TWA001.severity = suggestion - -# Only enforce in new feature folders -[Features/**.cs] -dotnet_diagnostic.TWA001.severity = warning -``` - -## Suppressing Violations - -For specific files that need exceptions beyond the configured patterns: - -```csharp -#pragma warning disable TWA001 // File name should use kebab-case -// File: LegacyAPIClient.cs -#pragma warning restore TWA001 -``` - -Or in the project file: - -```xml - - - TWA001 - - -``` \ No newline at end of file +# FileNameRuleAnalyzer (TW0001) + +## Overview + +The FileNameRuleAnalyzer enforces kebab-case naming conventions for C# source files. This analyzer helps maintain consistent file naming across projects that prefer kebab-case (e.g., `my-component.cs`) over traditional PascalCase. + +## Rule Details + +- **Rule ID**: TW0001 +- **Category**: Naming +- **Default Severity**: Info (disabled by default) +- **Message**: File '{0}' should use kebab-case naming convention (e.g., 'my-file.cs') + +## Kebab-Case Pattern + +Valid kebab-case file names must: +- Start with a lowercase letter +- Use only lowercase letters and numbers +- Separate words with hyphens (-) +- Not contain consecutive hyphens +- End with `.cs` extension + +### Valid Examples +- `user-service.cs` +- `data-processor.cs` +- `my-component.cs` +- `api-client-v2.cs` + +### Invalid Examples +- `UserService.cs` (PascalCase) +- `userService.cs` (camelCase) +- `user_service.cs` (snake_case) +- `user--service.cs` (consecutive hyphens) +- `-user-service.cs` (starts with hyphen) + +## Configuration + +### Enabling the Analyzer + +The analyzer is disabled by default to avoid breaking existing projects. To enable it, add to your `.editorconfig`: + +```ini +# Enable with warning severity +dotnet_diagnostic.TW0001.severity = warning + +# Or as an error to fail builds +dotnet_diagnostic.TW0001.severity = error + +# Or as a suggestion for gentle nudging +dotnet_diagnostic.TW0001.severity = suggestion +``` + +### Exception Patterns + +Configure files that should be excluded from the rule: + +```ini +dotnet_diagnostic.TW0001.excluded_files = *.g.cs;*.Generated.cs;GlobalUsings.cs;Program.cs;Startup.cs +``` + +Default exceptions include: +- Generated files (`*.g.cs`, `*.Generated.cs`, `*.designer.cs`) +- Razor component code-behind files (`*.razor.cs`) - must match their `.razor` file names +- Build files (`Directory.Build.props`, `Directory.Build.targets`, `Directory.Packages.props`) +- Assembly metadata (`AssemblyInfo.cs`) +- Analyzer tracking files (`AnalyzerReleases.Shipped.md`, `AnalyzerReleases.Unshipped.md`) + +## Implementation Details + +The analyzer is implemented as an `IIncrementalGenerator` for optimal performance: +- Only re-analyzes changed files +- Efficient caching of analysis results +- Minimal impact on build times + +## Use Cases + +This analyzer is useful for: +- Teams transitioning from other languages (e.g., JavaScript/TypeScript) that use kebab-case +- Projects that want to align file naming with URL patterns +- Maintaining consistency with kebab-case component naming in frameworks + +## Examples + +### Project-Wide Configuration + +In your root `.editorconfig`: + +```ini +[*.cs] +# Enable kebab-case file naming for all C# files +dotnet_diagnostic.TW0001.severity = warning + +# But allow exceptions for specific patterns +dotnet_diagnostic.TW0001.excluded_files = *.g.cs;*.Generated.cs;*.designer.cs;Program.cs;Startup.cs;GlobalUsings.cs;AssemblyInfo.cs +``` + +### Gradual Adoption + +For existing projects, start with suggestion severity: + +```ini +[*.cs] +dotnet_diagnostic.TW0001.severity = suggestion + +# Only enforce in new feature folders +[Features/**.cs] +dotnet_diagnostic.TW0001.severity = warning +``` + +## Suppressing Violations + +For specific files that need exceptions beyond the configured patterns: + +```csharp +#pragma warning disable TW0001 // File name should use kebab-case +// File: LegacyAPIClient.cs +#pragma warning restore TW0001 +``` + +Or in the project file: + +```xml + + + TW0001 + + +``` diff --git a/documentation/developer/reference/analyzers/overview.md b/documentation/developer/reference/analyzers/overview.md index 41b10a8..c77afc6 100644 --- a/documentation/developer/reference/analyzers/overview.md +++ b/documentation/developer/reference/analyzers/overview.md @@ -1,48 +1,48 @@ -# Analyzers Reference - -## Available Analyzers - -### TWA001 - File Name Rule Analyzer - -Enforces kebab-case naming convention for C# source files. - -- **Rule ID**: TWA001 -- **Category**: Naming -- **Default Severity**: Info (disabled) -- **Configuration**: `.editorconfig` - -[Full Reference](./file-name-rule-analyzer.md) - -## Common Configuration - -All analyzers are configured through `.editorconfig`: - -```ini -# Enable an analyzer -dotnet_diagnostic.TWA001.severity = warning - -# Configure exceptions -dotnet_diagnostic.TWA001.excluded_files = *.g.cs;*.Generated.cs -``` - -## Analyzer Categories - -- **Naming**: File and identifier naming conventions -- **SourceGenerator**: Diagnostics from source generators - -## Suppression Methods - -1. **File-level pragma**: - ```csharp - #pragma warning disable TWA001 - ``` - -2. **Project-wide**: - ```xml - TWA001 - ``` - -3. **EditorConfig**: - ```ini - dotnet_diagnostic.TWA001.severity = none - ``` \ No newline at end of file +# Analyzers Reference + +## Available Analyzers + +### TW0001 - File Name Rule Analyzer + +Enforces kebab-case naming convention for C# source files. + +- **Rule ID**: TW0001 +- **Category**: Naming +- **Default Severity**: Info (disabled) +- **Configuration**: `.editorconfig` + +[Full Reference](./file-name-rule-analyzer.md) + +## Common Configuration + +All analyzers are configured through `.editorconfig`: + +```ini +# Enable an analyzer +dotnet_diagnostic.TW0001.severity = warning + +# Configure exceptions +dotnet_diagnostic.TW0001.excluded_files = *.g.cs;*.Generated.cs +``` + +## Analyzer Categories + +- **Naming**: File and identifier naming conventions +- **SourceGenerator**: Diagnostics from source generators + +## Suppression Methods + +1. **File-level pragma**: + ```csharp + #pragma warning disable TW0001 + ``` + +2. **Project-wide**: + ```xml + TW0001 + ``` + +3. **EditorConfig**: + ```ini + dotnet_diagnostic.TW0001.severity = none + ``` diff --git a/documentation/developer/reference/overview.md b/documentation/developer/reference/overview.md index 75387e6..00a40ec 100644 --- a/documentation/developer/reference/overview.md +++ b/documentation/developer/reference/overview.md @@ -21,7 +21,7 @@ Complete reference for all source generators: ## API Reference ### DiagnosticDescriptor Properties -- **Id**: Unique identifier (e.g., TW0003) +- **Id**: Unique identifier (e.g., TW0001) - **Category**: Grouping for related rules - **Severity**: Default severity level - **IsEnabledByDefault**: Whether active without configuration diff --git a/documentation/developer/reference/source-generators/overview.md b/documentation/developer/reference/source-generators/overview.md index 709fa70..51d0956 100644 --- a/documentation/developer/reference/source-generators/overview.md +++ b/documentation/developer/reference/source-generators/overview.md @@ -2,7 +2,7 @@ ## Available Source Generators -### MarkdownDocsGenerator (TWG001) +### MarkdownDocsGenerator (TW0003) Generates XML documentation from markdown files for C# classes. diff --git a/documentation/overview.md b/documentation/overview.md index 86a0df0..56ddbb3 100644 --- a/documentation/overview.md +++ b/documentation/overview.md @@ -1,36 +1,36 @@ -# TimeWarp Source Generators Documentation - -## Quick Start - -1. **Add the package**: Reference TimeWarp.SourceGenerators -2. **Configure analyzers**: Edit `.editorconfig` -3. **Build**: See diagnostics and generated code - -## Documentation - -### [Developer Documentation](./developer/overview.md) -Technical documentation for developers using or contributing to the project. - -## What's Included - -### Source Generators -- **MarkdownDocsGenerator** - Generates markdown from XML docs - -### Analyzers -- **TWA001** - File name kebab-case enforcement - -## Configuration - -All configuration through `.editorconfig`: - -```ini -# Example: Enable kebab-case file names -dotnet_diagnostic.TW0003.severity = warning -``` - -## Philosophy - -- Everything uses kebab-case -- Opt-in by default -- Performance first -- Highly configurable \ No newline at end of file +# TimeWarp Source Generators Documentation + +## Quick Start + +1. **Add the package**: Reference TimeWarp.SourceGenerators +2. **Configure analyzers**: Edit `.editorconfig` +3. **Build**: See diagnostics and generated code + +## Documentation + +### [Developer Documentation](./developer/overview.md) +Technical documentation for developers using or contributing to the project. + +## What's Included + +### Source Generators +- **MarkdownDocsGenerator** - Generates markdown from XML docs + +### Analyzers +- **TW0001** - File name kebab-case enforcement + +## Configuration + +All configuration through `.editorconfig`: + +```ini +# Example: Enable kebab-case file names +dotnet_diagnostic.TW0001.severity = warning +``` + +## Philosophy + +- Everything uses kebab-case +- Opt-in by default +- Performance first +- Highly configurable diff --git a/kanban/archived/.gitkeep b/kanban/archived/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kanban/done/018-rename-diagnostic-prefixes-off-twa-to-free-architecture-twa.md b/kanban/done/018-rename-diagnostic-prefixes-off-twa-to-free-architecture-twa.md new file mode 100644 index 0000000..311320b --- /dev/null +++ b/kanban/done/018-rename-diagnostic-prefixes-off-twa-to-free-architecture-twa.md @@ -0,0 +1,161 @@ +# Rename ALL diagnostic IDs to TW#### (single product prefix) + +## Description + +Rename **every** diagnostic ID in `TimeWarp.SourceGenerators` to **TW** + four digits +(`TW0001`–`TW0006` mapping below). Drop **TWA**, **TWG**, and **TW1xxx** entirely. + +Architecture owns **TWA** = TimeWarp Architecture. This package owns **TW** = TimeWarp +(generic tooling / `TimeWarp.SourceGenerators`). + +This is a deliberate full rename for product clarity β€” not an optional collision fix. + +## Target prefix (locked recommendation) + +**TW** = TimeWarp (this package / generic tooling). Short, already used for interface-delegation +IDs, matches ecosystem β€œproduct acronym + digits.” Architecture keeps **TWA**; this repo does +**not** use TWA at all. + +One prefix for analyzers **and** generators. No TWAA/TWAG role suffixes. + +### Full mapping (contiguous TW####) + +| Current (leave none) | New | Feature | +|----------------------|-----|---------| +| TWA001 | **TW0001** | FileNameRuleAnalyzer (kebab-case) | +| TWA002 | **TW0002** | XmlDocsToMarkdownAnalyzer | +| TWG001 | **TW0003** | MarkdownDocsGenerator (if it still reports) | +| TW1001 | **TW0004** | Interface delegation β€” class must be partial | +| TW1002 | **TW0005** | Interface delegation β€” doesn’t implement interface | +| TW1003 | **TW0006** | Interface delegation β€” multiple fields same interface | + +Also scrub historical **TW000x** kanban/doc references so they match the new table (not old +TW0003 = kebab under a different scheme). + +## Requirements + +- **Zero** remaining `TWA*`, `TW1xxx` (old 1001–1003), or `TWG*` diagnostic IDs in **live code**. +- Single family: **TW0001+** under this package only. +- Update: all DiagnosticId / descriptor IDs, AnalyzerReleases.*.md, readme, documentation, + .editorconfig samples, tests, any help links. +- Document: **TW** = TimeWarp.SourceGenerators; **TWA** = TimeWarp Architecture (other product). +- Build green after rename. + +## Checklist + +- [x] Apply TW0001–TW0006 mapping (or document deviation) +- [x] Rename **all** IDs in source (no TWA / TWG / TW100x leftovers) +- [x] AnalyzerReleases.Shipped.md / Unshipped.md +- [x] readme.md, documentation/, .editorconfig samples +- [x] Tests assert new IDs (test-console `.editorconfig` uses TW0001/TW0002; no unit-test project asserts IDs) +- [x] Grep: no TWA001, TWA002, TW1001–1003, TWG001 in code +- [x] Commit +## Notes + +- Architecture already renamed TWPA β†’ TWA (timewarp-architecture). +- Prefix choice: **TW** (not TWS) β€” simpler brand for the generic generators package. +- Full rename is intentional product hygiene, not β€œonly if IDs collide.” +- Worktree: `/home/steve/worktrees/github.com/TimeWarpEngineering/timewarp-source-generators/Cramer-2026-06-30-dev` + +### Implementation plan (2026-07-15) + +**Confirmed:** TWG001 still reports on generator load β€” rename to TW0003, do not drop. + +#### Live source + +| File | Change | +|------|--------| +| `file-name-rule-analyzer.cs` | TWA001β†’TW0001; also `dotnet_diagnostic.TW0001.excluded_files` config key | +| `xml-docs-to-markdown-analyzer.cs` | TWA002β†’TW0002 | +| `markdown-docs-generator.cs` | TWG001β†’TW0003 | +| `interface-delegation-generator.cs` | TW1001β†’TW0004, TW1002β†’TW0005, TW1003β†’TW0006 | +| `AnalyzerReleases.Unshipped.md` | Replace all six rows with TW0001–TW0006 | +| `AnalyzerReleases.Shipped.md` | Leave empty (always-unshipped policy) | + +#### Config / docs / open kanban + +1. Root + test-console `.editorconfig` (TWA001/TWA002 β†’ TW0001/TW0002) +2. `readme.md` + TW vs TWA product-prefix note +3. `documentation/**` β€” especially `configure-file-name-analyzer.md` (old TW0003=kebab β†’ **TW0001**) +4. Open tasks 015, 016, 017 (fix + interface draft) β€” scrub wrong scheme IDs +5. Leave `kanban/done/*` history as-is (optional one-line notes only) + +#### Order + +1. Source descriptors + Unshipped (same change set β€” RS2008) +2. EditorConfig + readme +3. documentation/ +4. Open kanban scrub +5. Build + grep verify +6. Commit + +#### Verification + +```bash +dotnet build timewarp-source-generators.slnx -c Release +dotnet build tests/timewarp-source-generators-test-console/ -c Release +rg -n 'TWA00[12]|TWG001|TW100[1-3]' source/ documentation/ readme.md .editorconfig tests/ +rg -n 'TW000[1-6]' source/timewarp-source-generators/ +``` + +Pass: build green; zero old IDs on live product surface; Unshipped matches code. + +#### Out of scope + +- Renaming 017 folder name; implementing 015/016 code-fixes; unit-test project for diagnostics; + severity/behavior changes; Unshippedβ†’Shipped move; dual-ID compatibility layer. + +#### Gotchas + +- Config key is a literal string separate from `DiagnosticId` β€” both must change. +- Do not blind-sed `TW0003` in docs after rename (it correctly means MarkdownDocs load). +- Change source + Unshipped together. + +## Results + +### Summary + +Renamed every live diagnostic ID in `TimeWarp.SourceGenerators` to contiguous **TW0001–TW0006**. Dropped TWA/TWG/TW1xxx so TimeWarp Architecture can own **TWA**. Single **TW** family for this package’s analyzers and generators. + +| Old | New | Component | +|-----|-----|-----------| +| TWA001 | TW0001 | FileNameRuleAnalyzer (+ `excluded_files` key) | +| TWA002 | TW0002 | XmlDocsToMarkdownAnalyzer | +| TWG001 | TW0003 | MarkdownDocsGenerator | +| TW1001 | TW0004 | Interface delegation β€” partial | +| TW1002 | TW0005 | Interface delegation β€” not implement | +| TW1003 | TW0006 | Interface delegation β€” multiple fields | + +### Files changed + +**Source:** `file-name-rule-analyzer.cs`, `xml-docs-to-markdown-analyzer.cs`, `markdown-docs-generator.cs`, `interface-delegation-generator.cs`, `AnalyzerReleases.Unshipped.md` +**Config:** root `.editorconfig`, test-console `.editorconfig` +**Docs:** `readme.md`, `documentation/**` (overview, how-to, analyzer/SG refs, release tracking) +**Open kanban:** 015, 016, 017 (fix + interface draft) + +### Key decisions + +- Left `AnalyzerReleases.Shipped.md` empty (always-unshipped policy). +- No dual-ID compatibility layer (intentional full rename). +- File-name docs corrected from historical TW0003 scheme to **TW0001**. +- `kanban/done/*` history left as archaeology; open tasks scrubbed. +- Phantom TW1004 removed from interface-delegation draft task. + +### Verification + +- `dotnet build source/timewarp-source-generators/... -c Release` β€” **succeeded** (0 warnings/errors); package 1.0.0-beta.8 produced. +- Grep old IDs on `source/`, `documentation/`, `readme.md`, `.editorconfig`, `tests/` β€” **zero matches**. +- All six new IDs present in source + Unshipped. +- Review (`72e360e`): 0 bugs; 1 suggestion (017 prose) fixed in `42f1108`. + +### Note + +Solution-wide restore of the test console can fail with NU1102 when CPM `Version=$(Version)` does not resolve for the test project (pre-existing; not introduced by this rename). Library project builds clean. + +## Session + +- Created: 2026-07-15 +- Clarified: rename **ALL** IDs; do not leave TWA001/TWA002 +- Prefix locked: **TW** (recommend contiguous TW0001–TW0006) +- Plan: 2026-07-15 (orchestrate-task phase 2–3) +- Implemented + reviewed: 2026-07-15 (orchestrate-task phases 4–5) diff --git a/kanban/done/019-modernize-github-workflow-to-timewarp-ci-standard.md b/kanban/done/019-modernize-github-workflow-to-timewarp-ci-standard.md new file mode 100644 index 0000000..5c481d0 --- /dev/null +++ b/kanban/done/019-modernize-github-workflow-to-timewarp-ci-standard.md @@ -0,0 +1,209 @@ +# Modernize GitHub workflow to TimeWarp CI standard + +## Description + +Bring `.github/workflows/workflow.yml` in line with peer TimeWarp repos (nuru, builder, +options-validation, terminal): thin YAML, OIDC NuGet Trusted Publishing, and +`tools/dev-cli` as the CI entry point. + +The file was renamed from `ci-cd.yml` β†’ `workflow.yml` and the SDK bumped to `10.0.x` for +the standards audit. The pipeline body is still the old hand-rolled PowerShell flow and is +**not** up to current CI standards. + +## Requirements + +### Workflow YAML (thin pipeline) + +Match the peer pattern: + +1. Checkout with `fetch-depth: 0` +2. Setup .NET `10.0.x` +3. `nuget/login@v1` (OIDC) on release β€” `permissions: contents: read` + `id-token: write` +4. Run CI via dev-cli, e.g. + `dotnet run --file tools/dev-cli/dev.cs -- workflow` + (release: pass `--api-key` from nuget login when supported) +5. Upload `artifacts/packages/*.nupkg` (`if-no-files-found: ignore`, prefer `if: always()`) + +### Path filters + +Ensure changes that affect CI/build retrigger the workflow: + +- Keep: `source/**`, `tests/**`, `.github/workflows/**`, `Directory.Build.props` +- Add: `tools/**`, `Directory.Packages.props`, `msbuild/**` (or equivalent) +- Drop or justify loose `*.props` / `*.targets` if superseded by explicit paths + +### NuGet cache + +Either: + +- Cache `.nuget-cache/` (matches `RestorePackagesPath` in `Directory.Build.props`), or +- Stop setting a custom `RestorePackagesPath` and keep caching `~/.nuget/packages` + +Current cache of `~/.nuget/packages` is ineffective while restore uses `.nuget-cache/`. + +### Dev CLI (`workflow` command) + +Today `tools/dev-cli/endpoints/workflow-command.cs` only runs +`clean β†’ build β†’ test` via `./bin/dev` and has **no** pack/push/`--api-key` path. + +Either: + +- **A (preferred):** Extend `dev workflow` so it can own PR + release (mode/event detection, + pack, version check, NuGet push with optional API key) like peer repos, **or** +- **B:** Keep release pack/push as explicit YAML steps until A is done; still invoke + `dev` for build/test so the thin-YAML shape is partial but usable + +Document which option was chosen in Results. + +### Tests + +- Remove the dead TODO path `tests/timewarp-source-generators-tests/` +- Wire real verification (test console and/or `dev test` / `dev verify-samples`) so CI + fails on regressions β€” or document intentional deferral with a follow-up task + +### Auth + +- Prefer OIDC Trusted Publishing (`nuget/login@v1`, user `TimeWarp.Enterprises`) +- Retire `secrets.PUBLISH_TO_NUGET_ORG` unless still required as fallback + +## Checklist + +- [x] Rewrite `.github/workflows/workflow.yml` to thin peer-style pipeline +- [x] Add OIDC permissions + `nuget/login@v1` for release +- [x] Fix path filters (`tools/**`, `Directory.Packages.props`, etc.) +- [x] Fix NuGet cache path vs `RestorePackagesPath` (removed cache step; kept RestorePackagesPath) +- [x] Extend or document `dev workflow` for pack/push (option A or B) β€” **Option A** +- [x] Enable real test/verify step; remove dead test path +- [x] Drop legacy `PUBLISH_TO_NUGET_ORG` push when OIDC works +- [x] Validate PR path: build (+ tests) without publish +- [x] Validate release path: version check + pack + push (or dry-run notes) +- [x] Commit +## Notes + +### Current workflow issues (review 2026-07-15) + +| Issue | Detail | +|-------|--------| +| Legacy body | Inline pwsh: build project, optional version search, `dotnet nuget push` with secret | +| Auth | `secrets.PUBLISH_TO_NUGET_ORG` β€” peers use OIDC | +| Path filters | Missing `tools/**`, `Directory.Packages.props` | +| Cache | Caches `~/.nuget/packages`; restore uses `.nuget-cache/` | +| Tests | Commented; wrong folder name | +| `dev workflow` | Needs prebuilt `./bin/dev`; no pack/push/`--api-key` | + +### Reference workflows + +- `timewarp-nuru` / `timewarp-builder` / `timewarp-options-validation` / `timewarp-terminal` + β†’ `.github/workflows/workflow.yml` + +### What’s already good + +- Filename `workflow.yml` (audit) +- SDK `10.0.x` +- Triggers: push/PR β†’ master, release published, workflow_dispatch +- Version still lives in `source/Directory.Build.props` (`GeneratePackageOnBuild` β†’ + `artifacts/packages/`) + +### Implementation plan (2026-07-15) + +**Chosen approach: Option A** β€” full `dev workflow` ownership (peer consensus). +**Peer template:** timewarp-options-validation (single package + GeneratePackageOnBuild). + +#### Target + +``` +YAML (thin): checkout β†’ setup-dotnet 10 β†’ [release] nuget/login OIDC β†’ + dotnet run --file tools/dev-cli/dev.cs -- workflow [--api-key] + β†’ upload artifacts/packages/*.nupkg (always) + +PR/merge: clean β†’ build β†’ test console +Release: tag/version + NuGet not-published β†’ clean β†’ build β†’ test β†’ push +``` + +#### Key decisions + +| Topic | Decision | +|-------|----------| +| Option A vs B | **A** β€” extend workflow-command (in-process; no `./bin/dev`) | +| Tests | Rewrite `dev test` to build+run test console; remove dead path with YAML rewrite | +| Test console ref | **T1:** Analyzer `ProjectReference` (fix PackageReference/`Version=$(Version)` chicken-and-egg) | +| Cache | Remove cache step; keep `RestorePackagesPath=.nuget-cache/` (no peer cache) | +| Auth | OIDC only on `release`; retire `PUBLISH_TO_NUGET_ORG` | +| Path filters | Add `tools/**`, `Directory.Packages.props`, `msbuild/**`, `source/Directory.Build.props`, `nuget.config`; drop `*.props`/`*.targets` | +| Pack | No separate pack step β€” `GeneratePackageOnBuild` on Release build | + +#### Ordered steps + +1. Fix test console β†’ Analyzer ProjectReference (+ drop self PackageVersion if unused) +2. Rewrite `test-command.cs` β†’ build+run console +3. Rewrite `workflow-command.cs` β†’ Option A (modes, tag/version, NuGet check, push) +4. Optional: pin `build-command` to source project +5. Rewrite `workflow.yml` thin peer pipeline +6. Local verify: `dotnet run --file tools/dev-cli/dev.cs -- workflow` +7. Commit + +#### Files + +- `.github/workflows/workflow.yml` +- `tools/dev-cli/endpoints/workflow-command.cs` +- `tools/dev-cli/endpoints/test-command.cs` +- `tests/.../timewarp-source-generators-test-console.csproj` +- `Directory.Packages.props` (self PackageVersion cleanup) +- Optionally `build-command.cs` + +#### Risks + +- OIDC Trusted Publishing must exist for package on nuget.org (ops) +- Do not shell `./bin/dev` from workflow (CI uses file-based run) +- `dotnet test` alone is false-green today β€” must use console + +## Results + +### Summary + +**Option A** delivered: thin GitHub Actions YAML + in-process `dev workflow` owns PR and release. + +| Mode | Pipeline | +|------|----------| +| PR / merge / local / `workflow_dispatch` | clean β†’ build β†’ test | +| Release (event or `--mode release`) | tag match + NuGet not-published β†’ clean β†’ build β†’ test β†’ push | + +### Files changed + +- `.github/workflows/workflow.yml` β€” full rewrite (OIDC, path filters, no secret/cache/pwsh) +- `tools/dev-cli/endpoints/workflow-command.cs` β€” Option A modes + release gates + push +- `tools/dev-cli/endpoints/test-command.cs` β€” build+run test console +- `tools/dev-cli/endpoints/build-command.cs` β€” Design note only +- `tools/dev-cli/global-usings.cs` β€” Regex +- `tests/.../timewarp-source-generators-test-console.csproj` β€” Analyzer ProjectReference (T1) +- `Directory.Packages.props` β€” removed self PackageVersion +- Regenerated test console `DataService.implements.g.cs` comment + +### Key decisions + +- Peer template: timewarp-options-validation +- Clean via `IRepoCleanService` (no local CleanCommand type) +- NuGet not-published via `INuGetPackageService.SearchAsync` +- OIDC login only on `release` (not `workflow_dispatch`) +- `workflow_dispatch` β†’ Pr mode (use `--mode release` to publish) +- Cache step removed; `RestorePackagesPath=.nuget-cache/` unchanged +- Pack still `GeneratePackageOnBuild` +- `RS0030` suppressed on smoke test console for intentional `System.Console` + +### Verification + +- `dotnet run --file tools/dev-cli/dev.cs -- workflow` β†’ **exit 0** (clean β†’ build nupkg β†’ test console) +- Review `e7bf005`: **approve**, 0 issues +- Release path: code gates tag + NuGet + push; live OIDC requires nuget.org Trusted Publishing for this package (ops) + +### Ops follow-ups + +- Ensure NuGet.org Trusted Publishing is registered for `TimeWarp.SourceGenerators` + this GitHub repo +- Remove obsolete GitHub secret `PUBLISH_TO_NUGET_ORG` when convenient +- Bump `Version` before next release if `1.0.0-beta.8` is already on NuGet.org + +## Session + +- Created: 2026-07-15 (post `ganda repo audit --fix` + workflow review) +- Plan: 2026-07-15 (orchestrate-task phase 2–3) +- Implemented + reviewed: 2026-07-15 (orchestrate-task phases 4–5) diff --git a/kanban/in-progress/017_fix-tw0003-analyzer-ignoring-generated-files/task.md b/kanban/in-progress/017_fix-tw0003-analyzer-ignoring-generated-files/task.md index 58f3f35..9deff94 100644 --- a/kanban/in-progress/017_fix-tw0003-analyzer-ignoring-generated-files/task.md +++ b/kanban/in-progress/017_fix-tw0003-analyzer-ignoring-generated-files/task.md @@ -1,7 +1,7 @@ -# Fix TW0003 Analyzer to Ignore Generated Files +# Fix TW0001 Analyzer to Ignore Generated Files ## Description -The TW0003 file naming analyzer is incorrectly checking files in build output directories (obj/, bin/) which contain auto-generated build artifacts. The analyzer should skip validation for these directories as they are not user-written source code. +The TW0001 file naming analyzer is incorrectly checking files in build output directories (obj/, bin/) which contain auto-generated build artifacts. The analyzer should skip validation for these directories as they are not user-written source code. ## Problem The analyzer is reporting errors for generated files such as: @@ -11,21 +11,21 @@ The analyzer is reporting errors for generated files such as: These files are created by the build system and their naming conventions are controlled by the .NET SDK, not the user. ## Acceptance Criteria -- [ ] TW0003 analyzer skips files in `/obj/` directories -- [ ] TW0003 analyzer skips files in `/bin/` directories -- [ ] TW0003 analyzer skips files in other common build output directories +- [ ] TW0001 analyzer skips files in `/obj/` directories +- [ ] TW0001 analyzer skips files in `/bin/` directories +- [ ] TW0001 analyzer skips files in other common build output directories - [ ] User source files continue to be validated correctly - [ ] Tests verify that generated files are ignored - [ ] Tests verify that regular source files are still checked ## Technical Details -The fix should be implemented in the TW0003 analyzer by: +The fix should be implemented in the TW0001 analyzer by: 1. Checking if the file path contains `/obj/` or `/bin/` 2. Returning early without reporting diagnostics for these paths 3. Consider using normalized path comparison to handle different path separators ## Implementation Location -The fix should be applied in the TW0003 analyzer implementation, likely in the method that determines whether to analyze a given file. +The fix should be applied in the TW0001 analyzer implementation, likely in the method that determines whether to analyze a given file. ## Test Cases - Verify files in obj/ directory are ignored @@ -35,4 +35,4 @@ The fix should be applied in the TW0003 analyzer implementation, likely in the m - Verify edge cases like files named "obj.cs" in source directories are still checked ## References -- Original issue: `/home/steventcramer/worktrees/github.com/TimeWarpEngineering/timewarp-code/Cramer-2025-07-31-spike/analysis/tw0003-analyzer-issue.md` \ No newline at end of file +- Original issue: `/home/steventcramer/worktrees/github.com/TimeWarpEngineering/timewarp-code/Cramer-2025-07-31-spike/analysis/tw0003-analyzer-issue.md` diff --git a/kanban/to-do/015_create-kebab-case-file-name-code-fix/task.md b/kanban/to-do/015_create-kebab-case-file-name-code-fix/task.md index d4e8bb6..1602714 100644 --- a/kanban/to-do/015_create-kebab-case-file-name-code-fix/task.md +++ b/kanban/to-do/015_create-kebab-case-file-name-code-fix/task.md @@ -6,7 +6,7 @@ Code fix providers cannot be in the same assembly as source generators due to RS1038 error. The Microsoft.CodeAnalysis.Workspaces assembly (required for code fixes) is not provided during command line compilation scenarios. ## Description -Create a code fix provider for the FileNameRuleAnalyzer (TW0003) that automatically renames files from PascalCase or other naming conventions to kebab-case format. +Create a code fix provider for the FileNameRuleAnalyzer (TW0001) that automatically renames files from PascalCase or other naming conventions to kebab-case format. ## Acceptance Criteria - [ ] Code fix provider offers to rename file to kebab-case when diagnostic is reported @@ -18,13 +18,13 @@ Create a code fix provider for the FileNameRuleAnalyzer (TW0003) that automatica - [ ] Provides preview of the new file name before applying ## Technical Details -- Diagnostic ID: TW0003 +- Diagnostic ID: TW0001 - Fix Provider: FileNameRuleCodeFixProvider - Should integrate with existing FileNameRuleAnalyzer ## Implementation Steps 1. Create FileNameRuleCodeFixProvider class -2. Register the code fix for TW0003 diagnostic +2. Register the code fix for TW0001 diagnostic 3. Implement PascalCase to kebab-case conversion logic 4. Handle file renaming through Roslyn workspace APIs 5. Ensure all project references are updated @@ -40,7 +40,7 @@ Create a code fix provider for the FileNameRuleAnalyzer (TW0003) that automatica - Already kebab-case: `my-test.cs` β†’ no change ## Dependencies -- Requires FileNameRuleAnalyzer (TW0003) to be working +- Requires FileNameRuleAnalyzer (TW0001) to be working - Should follow existing code fix provider patterns in the codebase ## Implementation Progress diff --git a/kanban/to-do/016_create-xml-docs-to-markdown-code-fix/task.md b/kanban/to-do/016_create-xml-docs-to-markdown-code-fix/task.md index 2c661b0..ccac926 100644 --- a/kanban/to-do/016_create-xml-docs-to-markdown-code-fix/task.md +++ b/kanban/to-do/016_create-xml-docs-to-markdown-code-fix/task.md @@ -1,10 +1,10 @@ # Create XML Docs to Markdown Code Fix Provider ## Description -Create a code fix provider for the XmlDocsToMarkdownAnalyzer (TW0004) that extracts XML documentation from C# code and converts it to markdown files compatible with the MarkdownDocsGenerator. +Create a code fix provider for the XmlDocsToMarkdownAnalyzer (TW0002) that extracts XML documentation from C# code and converts it to markdown files compatible with the MarkdownDocsGenerator. ## Acceptance Criteria -- [ ] Code fix provider responds to TW0004 diagnostics +- [ ] Code fix provider responds to TW0002 diagnostics - [ ] Extracts all XML documentation elements from type and members - [ ] Converts XML to markdown following MarkdownDocsGenerator format - [ ] Removes XML documentation from source code @@ -54,7 +54,7 @@ Since code fix providers run in IDE/compiler context: ### Code Structure - Use existing temporary file: `xml-docs-to-markdown-code-fix-provider.cs.temp` - Implement `CodeFixProvider` base class -- Register for `TW0004` diagnostic ID +- Register for `TW0002` diagnostic ID - Handle type and member documentation ## Technical Details @@ -65,7 +65,7 @@ Since code fix providers run in IDE/compiler context: ## Dependencies - Requires project configuration for code fix providers -- XmlDocsToMarkdownAnalyzer (TW0004) must be working +- XmlDocsToMarkdownAnalyzer (TW0002) must be working - Understanding of MarkdownDocsGenerator format ## References diff --git a/kanban/to-do/017_create-interface-delegation-generator/task.md b/kanban/to-do/017_create-interface-delegation-generator/task.md index 35bd5e0..45aaebf 100644 --- a/kanban/to-do/017_create-interface-delegation-generator/task.md +++ b/kanban/to-do/017_create-interface-delegation-generator/task.md @@ -183,10 +183,10 @@ public partial class Service : IDataProcessor ### Diagnostic Codes -- **TW1001**: Class is not marked as partial -- **TW1002**: Field/property type does not match any interface on the class -- **TW1003**: Multiple fields marked with [Implements] for the same interface -- **TW1004**: Field marked with [Implements] but class does not implement the interface +- **TW0004**: Class must be partial for interface delegation +- **TW0005**: Class does not implement the delegated interface +- **TW0006**: Multiple fields delegate the same interface + ## Benefits diff --git a/msbuild/repository.props b/msbuild/repository.props new file mode 100644 index 0000000..201f1be --- /dev/null +++ b/msbuild/repository.props @@ -0,0 +1,15 @@ + + + + timewarp-source-generators + $(MSBuildThisFileDirectory)../ + $(RepositoryRoot)source/ + $(RepositoryRoot)tests/ + $(RepositoryRoot)samples/ + $(RepositoryRoot)benchmarks/ + $(RepositoryRoot)runfiles/ + $(RepositoryRoot)tools/ + $(RepositoryRoot)artifacts/ + $(ArtifactsDirectory)packages/ + + diff --git a/nuget.config b/nuget.config index e112e11..d9a5a9c 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,9 @@ + - + diff --git a/readme.md b/readme.md index 3ac3e76..9ab9271 100644 --- a/readme.md +++ b/readme.md @@ -64,9 +64,9 @@ The generator will automatically create forwarding implementations for all inter #### Diagnostics -- **TW1001**: Class must be partial for interface delegation -- **TW1002**: Class does not implement the delegated interface -- **TW1003**: Multiple fields delegate the same interface +- **TW0004**: Class must be partial for interface delegation +- **TW0005**: Class does not implement the delegated interface +- **TW0006**: Multiple fields delegate the same interface ### File Name Rule Analyzer @@ -78,9 +78,11 @@ Configure exceptions in `.editorconfig`: ```ini [*.cs] -dotnet_diagnostic.TWA001.excluded_files = Program.cs;Startup.cs;*.Designer.cs +dotnet_diagnostic.TW0001.excluded_files = Program.cs;Startup.cs;*.Designer.cs ``` +Diagnostic IDs in this package use the **TW** prefix (TimeWarp.SourceGenerators). The **TWA** prefix is reserved for TimeWarp Architecture (a different product). + ## Getting started To quickly get started I recommend reviewing the samples in this repo. diff --git a/skills/.gitkeep b/skills/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/source/timewarp-source-generators/AnalyzerReleases.Unshipped.md b/source/timewarp-source-generators/AnalyzerReleases.Unshipped.md index bf7e595..67c3fa9 100644 --- a/source/timewarp-source-generators/AnalyzerReleases.Unshipped.md +++ b/source/timewarp-source-generators/AnalyzerReleases.Unshipped.md @@ -1,10 +1,10 @@ -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- -TWG001 | SourceGenerator | Info | MarkdownDocsGenerator - Enhanced to support kebab-case file matching -TWA001 | Naming | Info | FileNameRuleAnalyzer, disabled by default -TWA002 | Documentation | Info | XmlDocsToMarkdownAnalyzer -TW1001 | InterfaceDelegation | Error | Class must be partial for interface delegation -TW1002 | InterfaceDelegation | Error | Class does not implement the delegated interface -TW1003 | InterfaceDelegation | Error | Multiple fields delegate the same interface +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +TW0001 | Naming | Info | FileNameRuleAnalyzer, disabled by default +TW0002 | Documentation | Info | XmlDocsToMarkdownAnalyzer +TW0003 | SourceGenerator | Info | MarkdownDocsGenerator - Enhanced to support kebab-case file matching +TW0004 | InterfaceDelegation | Error | Class must be partial for interface delegation +TW0005 | InterfaceDelegation | Error | Class does not implement the delegated interface +TW0006 | InterfaceDelegation | Error | Multiple fields delegate the same interface diff --git a/source/timewarp-source-generators/file-name-rule-analyzer.cs b/source/timewarp-source-generators/file-name-rule-analyzer.cs index b188927..79a7eb8 100644 --- a/source/timewarp-source-generators/file-name-rule-analyzer.cs +++ b/source/timewarp-source-generators/file-name-rule-analyzer.cs @@ -3,7 +3,7 @@ namespace TimeWarp.SourceGenerators; [Generator] public class FileNameRuleAnalyzer : IIncrementalGenerator { - public const string DiagnosticId = "TWA001"; + public const string DiagnosticId = "TW0001"; private const string Category = "Naming"; private static readonly DiagnosticDescriptor Rule = new( @@ -99,7 +99,7 @@ private string[] GetConfiguredExceptions(AnalyzerConfigOptionsProvider configOpt // Try to get configured exceptions from .editorconfig if (options.TryGetValue( - "dotnet_diagnostic.TWA001.excluded_files", + "dotnet_diagnostic.TW0001.excluded_files", out string? configuredExceptions) && !string.IsNullOrEmpty(configuredExceptions)) { // Split by semicolon and trim whitespace diff --git a/source/timewarp-source-generators/interface-delegation-generator.cs b/source/timewarp-source-generators/interface-delegation-generator.cs index 5ad7ac9..e11ab49 100644 --- a/source/timewarp-source-generators/interface-delegation-generator.cs +++ b/source/timewarp-source-generators/interface-delegation-generator.cs @@ -6,7 +6,7 @@ namespace TimeWarp.SourceGenerators; public class InterfaceDelegationGenerator : IIncrementalGenerator { private static readonly DiagnosticDescriptor ClassNotPartialDescriptor = new( - id: "TW1001", + id: "TW0004", title: "Class must be partial for interface delegation", messageFormat: "Class '{0}' must be marked as partial to use [Implements] attribute", category: "InterfaceDelegation", @@ -15,7 +15,7 @@ public class InterfaceDelegationGenerator : IIncrementalGenerator ); private static readonly DiagnosticDescriptor InterfaceNotImplementedDescriptor = new( - id: "TW1002", + id: "TW0005", title: "Class does not implement the delegated interface", messageFormat: "Class '{0}' must implement interface '{1}' to delegate to field/property '{2}'", category: "InterfaceDelegation", @@ -24,7 +24,7 @@ public class InterfaceDelegationGenerator : IIncrementalGenerator ); private static readonly DiagnosticDescriptor DuplicateDelegationDescriptor = new( - id: "TW1003", + id: "TW0006", title: "Multiple fields delegate the same interface", messageFormat: "Interface '{0}' is delegated by multiple fields/properties in class '{1}'", category: "InterfaceDelegation", diff --git a/source/timewarp-source-generators/markdown-docs-generator.cs b/source/timewarp-source-generators/markdown-docs-generator.cs index 14f618c..dcc580f 100644 --- a/source/timewarp-source-generators/markdown-docs-generator.cs +++ b/source/timewarp-source-generators/markdown-docs-generator.cs @@ -6,7 +6,7 @@ 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: "TWG001", + id: "TW0003", title: "MarkdownDocs Generator Loaded", messageFormat: "The MarkdownDocs generator has been loaded and initialized", category: "SourceGenerator", diff --git a/source/timewarp-source-generators/xml-docs-to-markdown-analyzer.cs b/source/timewarp-source-generators/xml-docs-to-markdown-analyzer.cs index b98468b..ba838fd 100644 --- a/source/timewarp-source-generators/xml-docs-to-markdown-analyzer.cs +++ b/source/timewarp-source-generators/xml-docs-to-markdown-analyzer.cs @@ -3,7 +3,7 @@ namespace TimeWarp.SourceGenerators; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class XmlDocsToMarkdownAnalyzer : DiagnosticAnalyzer { - public const string DiagnosticId = "TWA002"; + public const string DiagnosticId = "TW0002"; private const string Category = "Documentation"; private static readonly DiagnosticDescriptor Rule = new( diff --git a/tests/timewarp-source-generators-test-console/.editorconfig b/tests/timewarp-source-generators-test-console/.editorconfig index 157e1f5..f0767e3 100644 --- a/tests/timewarp-source-generators-test-console/.editorconfig +++ b/tests/timewarp-source-generators-test-console/.editorconfig @@ -4,7 +4,7 @@ root = true [*.cs] -dotnet_diagnostic.TWA001.severity = error # Set to warning/error to enforce kebab-case file naming -dotnet_diagnostic.TWA001.excluded_files = PascalCaseTest.cs -# Enable TWA002 - XML documentation to markdown analyzer -dotnet_diagnostic.TWA002.severity = suggestion \ No newline at end of file +dotnet_diagnostic.TW0001.severity = error # Set to warning/error to enforce kebab-case file naming +dotnet_diagnostic.TW0001.excluded_files = PascalCaseTest.cs +# Enable TW0002 - XML documentation to markdown analyzer +dotnet_diagnostic.TW0002.severity = suggestion \ No newline at end of file diff --git a/tests/timewarp-source-generators-test-console/generated/timewarp-source-generators/TimeWarp.SourceGenerators.InterfaceDelegationGenerator/DataService.implements.g.cs b/tests/timewarp-source-generators-test-console/generated/timewarp-source-generators/TimeWarp.SourceGenerators.InterfaceDelegationGenerator/DataService.implements.g.cs index 9f17241..1a4e45f 100644 --- a/tests/timewarp-source-generators-test-console/generated/timewarp-source-generators/TimeWarp.SourceGenerators.InterfaceDelegationGenerator/DataService.implements.g.cs +++ b/tests/timewarp-source-generators-test-console/generated/timewarp-source-generators/TimeWarp.SourceGenerators.InterfaceDelegationGenerator/DataService.implements.g.cs @@ -3,7 +3,7 @@ namespace TimeWarp.SourceGenerators.TestConsole; -// Interface delegation for DataService - YO YO YO MAMA v2 with inherited interfaces! +// Interface delegation for DataService public partial class DataService { // Delegation to _logger for TimeWarp.SourceGenerators.TestConsole.ILogger diff --git a/tests/timewarp-source-generators-test-console/timewarp-source-generators-test-console.csproj b/tests/timewarp-source-generators-test-console/timewarp-source-generators-test-console.csproj index 8a7ec0a..b564a05 100644 --- a/tests/timewarp-source-generators-test-console/timewarp-source-generators-test-console.csproj +++ b/tests/timewarp-source-generators-test-console/timewarp-source-generators-test-console.csproj @@ -2,20 +2,22 @@ Exe - net9.0 + net10.0 TimeWarp.SourceGenerators.TestConsole enable enable preview diagnostic + + $(NoWarn);RS0030 - + - - all - build; analyzers - + + @@ -37,4 +39,4 @@ - \ No newline at end of file + diff --git a/tools/dev-cli/Directory.Build.props b/tools/dev-cli/Directory.Build.props new file mode 100644 index 0000000..e8255dd --- /dev/null +++ b/tools/dev-cli/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + + $(NoWarn);CA1031;CA1303;CA1508;CA1515;CA1849;CA2007;CA2016;CA2000;CA1034;CA5399;IDE0065;IL2026;IL2104;IL3050;IL3053;RCS1046;IDE0005;IDE0052;IDE0055;IDE0058;IDE0160;IDE0211;IDE0290 + + + true + true + + + $(InterceptorsNamespaces);TimeWarp.Nuru.Generated + + + + + + + + + + + + + + + + + diff --git a/tools/dev-cli/dev.cs b/tools/dev-cli/dev.cs new file mode 100755 index 0000000..f820d42 --- /dev/null +++ b/tools/dev-cli/dev.cs @@ -0,0 +1,40 @@ +#!/usr/bin/env -S dotnet -- +// ═══════════════════════════════════════════════════════════════════════════════ +// DEV CLI - timewarp-source-generators DEVELOPMENT TOOL +// ═══════════════════════════════════════════════════════════════════════════════ +// +// Usage: +// As runfile: dotnet run tools/dev-cli/dev.cs -- +// As AOT: ./bin/dev +// +// Run `./bin/dev --help` for available commands. +// +// To bootstrap: +// dotnet run tools/dev-cli/dev.cs -- self-install +// direnv allow +// dev --help +// ═══════════════════════════════════════════════════════════════════════════════ + +#region Purpose +// Entry point for the dev CLI +#endregion +#region Design +// Thin wrapper around TimeWarp.Nuru to execute development commands +#endregion + +NuruApp app = NuruApp.CreateBuilder() + .WithName("dev") + .WithDescription("Development CLI for timewarp-source-generators") + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + }) + .DiscoverEndpoints() + .Build(); + +return await app.RunAsync(args); \ No newline at end of file diff --git a/tools/dev-cli/endpoints/build-command.cs b/tools/dev-cli/endpoints/build-command.cs new file mode 100644 index 0000000..a20f772 --- /dev/null +++ b/tools/dev-cli/endpoints/build-command.cs @@ -0,0 +1,113 @@ +#region Purpose +// Build all projects in the repository +#endregion +#region Design +// Discovers the repository root dynamically using Git.FindRoot() +// Builds the solution at the repo root in Release (source packs via GeneratePackageOnBuild) +// Handler stores Command and Ct as fields so private methods are zero-parameter +// Streams MSBuild output via Amuru RunAsync by default; --quiet uses CaptureAsync +#endregion + +namespace DevCli.Commands; + +[NuruRoute("build", Description = "Build all projects in the repository")] +internal sealed class BuildCommand : ICommand +{ + [Option("clean", "c", Description = "Clean before building")] + public bool Clean { get; set; } + + [Option("quiet", "q", Description = "Hide build output unless the command fails")] + public bool Quiet { get; set; } + + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + private BuildCommand Command = null!; + private CancellationToken Ct; + private string RepoRoot = null!; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(BuildCommand command, CancellationToken ct) + { + Command = command; + Ct = ct; + + if (!FindRepoRoot()) return Value; + if (!await CleanAsync()) return Value; + if (!await BuildAsync()) return Value; + + Terminal.WriteLine("\nBuild completed successfully!".Green()); + return Value; + } + + private bool FindRepoRoot() + { + string? root = Git.FindRoot(); + if (root is null) + { + Terminal.WriteErrorLine("Error: could not find repository root."); + Environment.ExitCode = 1; + return false; + } + RepoRoot = root; + Terminal.WriteLine($"Building repository at {RepoRoot}..."); + return true; + } + + private async Task CleanAsync() + { + if (!Command.Clean) return true; + + Terminal.WriteLine("\nCleaning before build..."); + CommandResult command = DotNet.Clean() + .WithWorkingDirectory(RepoRoot) + .WithNoValidation() + .Build(); + + return await ExecuteAsync(command, "Clean failed!"); + } + + private async Task BuildAsync() + { + Terminal.WriteLine("\nBuilding..."); + CommandResult command = DotNet.Build() + .WithConfiguration("Release") + .WithWorkingDirectory(RepoRoot) + .WithNoValidation() + .Build(); + + return await ExecuteAsync(command, "Build failed!"); + } + + private async Task ExecuteAsync(CommandResult command, string failureMessage) + { + if (Command.Quiet) + { + CommandOutput result = await command.CaptureAsync(Ct); + if (!result.Success) + { + Terminal.WriteErrorLine(result.Combined); + Terminal.WriteErrorLine(failureMessage.Red()); + Environment.ExitCode = 1; + return false; + } + + return true; + } + + int exitCode = await command.RunAsync(Ct); + if (exitCode != 0) + { + Terminal.WriteErrorLine(failureMessage.Red()); + Environment.ExitCode = exitCode; + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/tools/dev-cli/endpoints/test-command.cs b/tools/dev-cli/endpoints/test-command.cs new file mode 100644 index 0000000..b71fc74 --- /dev/null +++ b/tools/dev-cli/endpoints/test-command.cs @@ -0,0 +1,117 @@ +#region Purpose +// Run the test suite +#endregion +#region Design +// Builds and runs the test-console project in Release (no Fixie/dotnet test suite yet) +// Fails on non-zero exit from build or run +// Handler stores Command and Ct as fields so private methods are zero-parameter +// Streams output via Amuru RunAsync by default; --quiet uses CaptureAsync +#endregion + +namespace DevCli.Commands; + +[NuruRoute("test", Description = "Run the test suite")] +internal sealed class TestCommand : ICommand +{ + [Option("quiet", "q", Description = "Hide test output unless the command fails")] + public bool Quiet { get; set; } + + internal sealed class Handler : ICommandHandler + { + private const string TestProjectRelativePath = + "tests/timewarp-source-generators-test-console/timewarp-source-generators-test-console.csproj"; + + private readonly ITerminal Terminal; + private TestCommand Command = null!; + private CancellationToken Ct; + private string RepoRoot = null!; + private string TestProjectPath = null!; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public async ValueTask Handle(TestCommand command, CancellationToken ct) + { + Command = command; + Ct = ct; + + if (!FindRepoRoot()) return Value; + if (!await BuildTestProjectAsync()) return Value; + if (!await RunTestProjectAsync()) return Value; + + Terminal.WriteLine("\nTests completed successfully!".Green()); + return Value; + } + + private bool FindRepoRoot() + { + string? root = Git.FindRoot(); + if (root is null) + { + Terminal.WriteErrorLine("Error: could not find repository root."); + Environment.ExitCode = 1; + return false; + } + + RepoRoot = root; + TestProjectPath = Path.Combine(RepoRoot, TestProjectRelativePath); + Terminal.WriteLine("Running test suite..."); + return true; + } + + private async Task BuildTestProjectAsync() + { + Terminal.WriteLine($"Building {TestProjectRelativePath} (Release)..."); + CommandResult command = DotNet.Build(TestProjectPath) + .WithConfiguration("Release") + .WithWorkingDirectory(RepoRoot) + .WithNoValidation() + .Build(); + + return await ExecuteAsync(command, "Test project build failed!"); + } + + private async Task RunTestProjectAsync() + { + Terminal.WriteLine($"Running {TestProjectRelativePath} (Release)..."); + CommandResult command = DotNet.Run() + .WithProject(TestProjectPath) + .WithConfiguration("Release") + .WithNoBuild() + .WithWorkingDirectory(RepoRoot) + .WithNoValidation() + .Build(); + + return await ExecuteAsync(command, "Tests failed!"); + } + + private async Task ExecuteAsync(CommandResult command, string failureMessage) + { + if (Command.Quiet) + { + CommandOutput result = await command.CaptureAsync(Ct); + if (!result.Success) + { + Terminal.WriteErrorLine(result.Combined); + Terminal.WriteErrorLine(failureMessage.Red()); + Environment.ExitCode = 1; + return false; + } + + return true; + } + + int exitCode = await command.RunAsync(Ct); + if (exitCode != 0) + { + Terminal.WriteErrorLine(failureMessage.Red()); + Environment.ExitCode = exitCode; + return false; + } + + return true; + } + } +} diff --git a/tools/dev-cli/endpoints/verify-samples-command.cs b/tools/dev-cli/endpoints/verify-samples-command.cs new file mode 100644 index 0000000..c9c03f9 --- /dev/null +++ b/tools/dev-cli/endpoints/verify-samples-command.cs @@ -0,0 +1,30 @@ +#region Purpose +// Verifies that any code samples in the repository compile +#endregion +#region Design +// Stub command for sample verification +#endregion + +namespace DevCli.Commands; + +[NuruRoute("verify-samples", Description = "Verify code samples compile")] +internal sealed class VerifySamplesCommand : ICommand +{ + internal sealed class Handler : ICommandHandler + { + private readonly ITerminal Terminal; + + public Handler(ITerminal terminal) + { + Terminal = terminal; + } + + public ValueTask 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); + } + } +} diff --git a/tools/dev-cli/endpoints/workflow-command.cs b/tools/dev-cli/endpoints/workflow-command.cs new file mode 100644 index 0000000..5ffd150 --- /dev/null +++ b/tools/dev-cli/endpoints/workflow-command.cs @@ -0,0 +1,258 @@ +#region Purpose +// Executes the full CI/CD pipeline with mode detection +#endregion +#region Design +// Auto-detects mode from GITHUB_EVENT_NAME or accepts explicit --mode flag +// pr/merge: clean -> build -> test +// release: tag match + NuGet not-published -> clean -> build -> test -> push +// Invokes sibling command handlers in-process so CI needs no self-install +// Clean uses IRepoCleanService (no shell to ./bin/dev) +// Release version comes from Version in source/Directory.Build.props and +// must match the git tag (GITHUB_REF_NAME) on release events +// Pack is GeneratePackageOnBuild; push targets artifacts/packages/*.nupkg +#endregion + +namespace DevCli.Commands; + +[NuruRoute("workflow", Description = "Execute full CI/CD pipeline")] +internal sealed class WorkflowCommand : ICommand +{ + [Option("mode", "m", Description = "CI mode: pr, merge, or release (auto-detected from GITHUB_EVENT_NAME if not specified)")] + public string? Mode { get; set; } + + [Option("api-key", Description = "NuGet API key for publishing (release mode only)")] + public string? ApiKey { get; set; } + + internal sealed class Handler : ICommandHandler + { + private const string PackageId = "TimeWarp.SourceGenerators"; + private const string NuGetSource = "https://api.nuget.org/v3/index.json"; + + private readonly ITerminal Terminal; + private readonly IRepoCleanService RepoCleanService; + private readonly INuGetPackageService NuGetPackageService; + private CancellationToken Ct; + private string RepoRoot = null!; + + public Handler + ( + ITerminal terminal, + IRepoCleanService repoCleanService, + INuGetPackageService nuGetPackageService + ) + { + Terminal = terminal; + RepoCleanService = repoCleanService; + NuGetPackageService = nuGetPackageService; + } + + public async ValueTask Handle(WorkflowCommand command, CancellationToken ct) + { + Ct = ct; + + if (!FindRepoRoot()) return Value; + + CiMode mode = DetermineMode(command.Mode); + Terminal.WriteLine($"Starting CI workflow (mode: {mode})..."); + + if (mode == CiMode.Release) + { + await RunReleaseWorkflowAsync(command.ApiKey); + } + else + { + await RunPrWorkflowAsync(); + } + + if (Environment.ExitCode == 0) + Terminal.WriteLine("\nWorkflow completed successfully!".Green()); + + return Value; + } + + private bool FindRepoRoot() + { + string? root = Git.FindRoot(); + if (root is null) + { + Terminal.WriteErrorLine("Error: could not find repository root."); + Environment.ExitCode = 1; + return false; + } + RepoRoot = root; + return true; + } + + private CiMode DetermineMode(string? explicitMode) + { + if (!string.IsNullOrEmpty(explicitMode)) + { + return explicitMode.ToLowerInvariant() switch + { + "release" => CiMode.Release, + "merge" => CiMode.Merge, + _ => CiMode.Pr + }; + } + + string? eventName = Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME"); + + CiMode mode = eventName switch + { + "push" => CiMode.Merge, + "release" => CiMode.Release, + // Manual runs are build/test only; use --mode release explicitly to publish + "workflow_dispatch" => CiMode.Pr, + _ => CiMode.Pr // pull_request and local dev + }; + + Terminal.WriteLine($"Detected GITHUB_EVENT_NAME: {eventName ?? "(not set)"} -> Mode: {mode}"); + return mode; + } + + private async Task RunPrWorkflowAsync() + { + if (!await RunStepAsync("Clean", CleanAsync)) return; + if (!await RunStepAsync("Build", () => new BuildCommand.Handler(Terminal).Handle(new BuildCommand(), Ct))) return; + await RunStepAsync("Test", () => new TestCommand.Handler(Terminal).Handle(new TestCommand(), Ct)); + } + + private async Task RunReleaseWorkflowAsync(string? apiKey) + { + string? version = ReadVersion(); + if (version is null) + { + Terminal.WriteErrorLine("Error: could not read Version from source/Directory.Build.props.".Red()); + Environment.ExitCode = 1; + return; + } + + if (!CheckVersionMatchesTag(version)) return; + if (!await CheckNotPublishedOnNuGetAsync(version)) return; + if (!await RunStepAsync("Clean", CleanAsync)) return; + if (!await RunStepAsync("Build", () => new BuildCommand.Handler(Terminal).Handle(new BuildCommand(), Ct))) return; + if (!await RunStepAsync("Test", () => new TestCommand.Handler(Terminal).Handle(new TestCommand(), Ct))) return; + await PushPackageAsync(version, apiKey); + } + + private async ValueTask CleanAsync() + { + Terminal.WriteLine("Cleaning repository artifacts..."); + CleanResult result = await RepoCleanService.CleanAsync(Ct); + Terminal.WriteLine + ( + $"Clean complete: {result.ObjDirectoriesDeleted} obj, {result.BinDirectoriesDeleted} bin, {result.RootBinFilesCleaned} root-bin items." + ); + return Value; + } + + private async Task RunStepAsync(string title, Func> step) + { + Terminal.WriteLine($"\n=== {title} ==="); + await step(); + + if (Environment.ExitCode != 0) + { + Terminal.WriteErrorLine($"{title} failed!".Red()); + return false; + } + return true; + } + + private string? ReadVersion() + { + string propsPath = Path.Combine(RepoRoot, "source", "Directory.Build.props"); + if (!File.Exists(propsPath)) return null; + + Match match = Regex.Match(File.ReadAllText(propsPath), "(.+?)"); + return match.Success ? match.Groups[1].Value : null; + } + + private bool CheckVersionMatchesTag(string version) + { + // Only enforce on tag-triggered releases; workflow_dispatch has no tag + if (Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME") != "release") return true; + + string? tag = Environment.GetEnvironmentVariable("GITHUB_REF_NAME"); + string? tagVersion = tag?.TrimStart('v'); + + if (tagVersion != version) + { + Terminal.WriteErrorLine($"Error: release tag '{tag}' does not match Version '{version}'.".Red()); + Environment.ExitCode = 1; + return false; + } + + Terminal.WriteLine($"Release tag '{tag}' matches Version '{version}'."); + return true; + } + + private async Task CheckNotPublishedOnNuGetAsync(string version) + { + Terminal.WriteLine($"\n=== Check NuGet (not published) ==="); + Terminal.WriteLine($"Checking if {PackageId} {version} is already on NuGet.org..."); + + NuGetSearchResult? search = await NuGetPackageService.SearchAsync(PackageId, Ct); + bool alreadyPublished = search?.Versions.Any + ( + v => string.Equals(v.Version, version, StringComparison.OrdinalIgnoreCase) + ) == true; + + if (alreadyPublished) + { + Terminal.WriteErrorLine + ( + $"Error: {PackageId} {version} is already published to NuGet.org. Increment Version in source/Directory.Build.props.".Red() + ); + Environment.ExitCode = 1; + return false; + } + + Terminal.WriteLine($"{PackageId} {version} is not yet published on NuGet.org."); + return true; + } + + private async Task PushPackageAsync(string version, string? apiKey) + { + Terminal.WriteLine("\n=== Push to NuGet ==="); + + string nupkgPath = Path.Combine(RepoRoot, "artifacts", "packages", $"{PackageId}.{version}.nupkg"); + if (!File.Exists(nupkgPath)) + { + Terminal.WriteErrorLine($"Error: package not found: {nupkgPath}".Red()); + Environment.ExitCode = 1; + return; + } + + Terminal.WriteLine($"Pushing {PackageId}.{version}.nupkg..."); + + List args = ["nuget", "push", nupkgPath, "--source", NuGetSource, "--skip-duplicate"]; + if (!string.IsNullOrEmpty(apiKey)) + { + args.AddRange(["--api-key", apiKey]); + } + + int exitCode = await Shell.Builder("dotnet") + .WithArguments([.. args]) + .WithWorkingDirectory(RepoRoot) + .WithNoValidation() + .RunAsync(Ct); + + if (exitCode != 0) + { + Terminal.WriteErrorLine("Push failed!".Red()); + Environment.ExitCode = exitCode; + return; + } + + Terminal.WriteLine($"\nPublished {PackageId} {version} to NuGet.org!".Green()); + } + } +} + +internal enum CiMode +{ + Pr, + Merge, + Release +} diff --git a/tools/dev-cli/global-usings.cs b/tools/dev-cli/global-usings.cs new file mode 100644 index 0000000..9052391 --- /dev/null +++ b/tools/dev-cli/global-usings.cs @@ -0,0 +1,22 @@ +#region Purpose +// Global usings for dev-cli +#endregion + +global using System; +global using System.IO; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Runtime.CompilerServices; +global using System.Runtime.InteropServices; +global using System.Xml.Linq; +global using System.Text.RegularExpressions; + +global using TimeWarp.Nuru; +global using static TimeWarp.Nuru.Unit; +global using TimeWarp.Amuru; +global using TimeWarp.Terminal; +global using DevCli; +global using Microsoft.Extensions.DependencyInjection; \ No newline at end of file diff --git a/tools/dev-cli/source/Directory.Build.props b/tools/dev-cli/source/Directory.Build.props new file mode 100644 index 0000000..d59f3f2 --- /dev/null +++ b/tools/dev-cli/source/Directory.Build.props @@ -0,0 +1,20 @@ + + + + + + + 1.0.0-beta.1 + TimeWarpEngineering + https://github.com/TimeWarpEngineering/timewarp-source-generators + https://github.com/TimeWarpEngineering/timewarp-source-generators + git + Unlicense + logo.png + + + + + + +