diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..b83dcd1
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+insert_final_newline = true
diff --git a/.gitattributes b/.gitattributes
index 1ff0c42..6f3fcbd 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,7 +1,7 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
-* text=auto
+* text=auto eol=lf
###############################################################################
# Set default behavior for command prompt diff.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 70149cb..90d71cb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,9 +19,12 @@ jobs:
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
+ - name: Verify solution structure
+ shell: pwsh
+ run: ./scripts/Verify-SolutionStructure.ps1
- name: Restore
- run: dotnet restore src/ChessRealms.ChessEngine.sln --locked-mode
+ run: dotnet restore ChessRealms.ChessEngine.slnx --locked-mode
- name: Release build
- run: dotnet build src/ChessRealms.ChessEngine.sln --configuration Release --no-restore
+ run: dotnet build ChessRealms.ChessEngine.slnx --configuration Release --no-restore
- name: Fast tests
- run: dotnet test src/ChessRealms.ChessEngine.sln --configuration Release --no-build --filter "TestCategory!=Deep"
+ run: dotnet test ChessRealms.ChessEngine.slnx --configuration Release --no-build --filter "TestCategory!=Deep"
diff --git a/.gitignore b/.gitignore
index 9491a2f..eaa9902 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,4 +360,4 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
\ No newline at end of file
+FodyWeavers.xsd
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9460ae6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,11 @@
+# Repository instructions
+
+- Follow [CONTRIBUTING.md](CONTRIBUTING.md).
+- Keep the physical repository layout and solution structure in sync.
+- Run `pwsh -NoProfile -File scripts/Verify-SolutionStructure.ps1` when changing
+ docs, scripts, the solution, or the set or location of projects.
+- Use LF line endings and respect `.editorconfig` and `.gitattributes`.
+- Run checks appropriate to the changes, following CONTRIBUTING.md.
+- In the final response, include proposed commit and PR titles, check results,
+ and validation limits. Providing titles does not authorize an automatic
+ commit, push, or PR creation.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6d6032e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,132 @@
+# Contributing
+
+## Prerequisites and checks
+
+Install Git, PowerShell 7 (`pwsh`), and .NET SDK **10.0.401**, the exact stable
+version pinned in `global.json`. Restore needs access to NuGet.org or a cache
+containing the locked packages. No additional PowerShell modules are required.
+
+Run these commands from the repository root:
+
+```sh
+pwsh -NoProfile -File scripts/Verify-SolutionStructure.ps1
+dotnet sln ChessRealms.ChessEngine.slnx list
+dotnet restore ChessRealms.ChessEngine.slnx --locked-mode
+dotnet build ChessRealms.ChessEngine.slnx -c Release --no-restore
+dotnet test ChessRealms.ChessEngine.slnx -c Release --no-build --filter "TestCategory!=Deep"
+git diff --check
+```
+
+For the ordinary developer loop, run `dotnet test` from the root. Run the structure
+check whenever docs, scripts, the solution, or project membership/locations change.
+For code, project, solution, SDK, dependency, or build/CI changes, also run locked
+restore, the Release build, and the fast tests above. Documentation-only changes
+need the structure check, link/command review, and `git diff --check`.
+
+Deep tests and performance runs are opt-in when relevant to the change; see
+[README.md](README.md) for commands. CI checks structure before locked restore,
+Release build, and fast tests on both Windows and Linux. Report local results as
+local results; do not infer a CI, Linux, or Visual Studio pass from them.
+
+## Repository and solution structure
+
+- `ChessRealms.ChessEngine.slnx`: the root solution in XML `.slnx` format.
+- `src/`: six projects (engine, tests, console, perft runner, benchmarks, and
+ magic-number search). Each project stays in its own physical directory.
+- `docs/`: API documentation, support boundaries, and historical validation records.
+- `scripts/`: repository tooling, initially only `Verify-SolutionStructure.ps1`.
+- `.github/workflows/ci.yml`: the Windows/Linux verification workflow.
+- Root files such as `README.md`, `CONTRIBUTING.md`, `AGENTS.md`, `global.json`,
+ and `Directory.Build.props`: repository instructions and shared configuration.
+
+Keep solution folders synchronized whenever files or projects are added, removed,
+renamed, or moved:
+
+- `docs` and `scripts` mirror their physical subdirectories recursively. Add each
+ tracked or new nonignored file once as a Solution Item in the matching folder,
+ using its full path relative to the root solution in a `` entry.
+ For example, `` belongs in
+ ``. Do not add empty folders
+ or ignored artifacts. Tracked files remain checked even if an ignore rule matches.
+- Put all `.csproj` files under the matching `src` solution structure. A project
+ node represents its own directory, so `src/Example/Example.csproj` belongs directly
+ in `src`, and `src/tools/Example/Example.csproj` belongs in `src/tools`. A project
+ directly in `src` also belongs in `src`. Do not duplicate project source files as
+ Solution Items. Preserve existing project/folder `Id` values, project types,
+ and build configurations. The standard Debug/Release and Any CPU mappings use
+ SLNX defaults, verified against the previous solution during migration.
+- Match spelling and case exactly, including on Windows. Both slash styles in
+ solution paths are supported. SLNX folder elements are direct children of
+ ``; their absolute names, such as `/docs/guides/`, define nesting.
+ Ancestor folders may be implicit. Record case-only renames in Git as well.
+
+Keep a single root solution so `dotnet test` can discover it without ambiguity.
+The SDK's `dotnet sln migrate` command converts a classic solution to
+SLNX, but leaves the old file in place. After validating the migration, remove
+the old solution and update current commands. See the
+[official CLI documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-sln).
+
+Run the structure checker after editing the solution. It uses its own location
+to find the repository, works from any current directory, reports discrepancies,
+and exits nonzero on failure. It never fixes files automatically. Keep historical
+reports accurate: preserve commands and results as originally run and add a note
+pointing to current instructions when paths change. Root service files stay at
+the root and do not need entries in `docs`.
+
+## Namespaces and project names
+
+`ChessRealms` is the organization prefix, matching the configured GitHub origin
+`ChessRealms/ChessEngine`. `ChessRealms.ChessEngine` identifies the library;
+suffixes such as `.Tests`, `.Console`, `.Perft`, and `.Benchmark` identify related
+projects by purpose. `ChessRealms.MagicBruteforce` is a supporting tool under the
+same organization. This follows the
+[.NET namespace naming guidance](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-namespaces).
+
+Keep these names consistent across namespaces, project/assembly names, project
+references, and `InternalsVisibleTo` declarations. Removing the organization prefix
+would change public type names and, if projects are renamed, assembly identities.
+Treat such a rename as an intentional compatibility change with a migration plan.
+There is no technical requirement to rename them when changing solution format.
+
+## Branches, commits, and pull requests
+
+Use `/` for branches. Use
+`(): ` for commit and PR titles; choose
+a concise scope such as `repo`, `engine`, `tests`, or `ci`.
+
+| Branch type | Purpose | Commit / PR prefix |
+| --- | --- | --- |
+| `feature` | Add user-facing functionality | `feat` |
+| `fix` | Correct a defect | `fix` |
+| `docs` | Update documentation | `docs` |
+| `test` | Add or improve tests | `test` |
+| `refactor` | Restructure code without changing behavior | `refactor` |
+| `perf` | Improve performance | `perf` |
+| `ci` | Change continuous integration | `ci` |
+| `chore` | Maintain tooling or repository organization | `chore` |
+
+The branch type is **`feature`**, while the corresponding commit and PR type is
+**`feat`**. All other branch types use the same prefix in commit and PR titles.
+For example:
+
+- `feature/add-uci-support` → `feat(engine): add UCI support`
+- `fix/validate-promotion` → `fix(engine): validate promotion choices`
+- `docs/update-api-guide` → `docs(api): clarify draw claims`
+- `chore/repository-organization` →
+ `chore(repo): organize solution, documentation and repository checks`
+
+Describe the problem, resulting behavior, and scope in the PR. Include relevant
+design choices or compatibility implications, the exact checks run and their
+results (including test counts where available), and any checks not run or other
+validation limits. Link a related issue when one exists. Keep the title and
+description aligned with the final changes reviewers will see.
+
+## Text files
+
+Use UTF-8, LF line endings, and a final newline, following `.editorconfig` and
+`.gitattributes`. Preserve existing indentation unless a change requires otherwise.
+Git automatically classifies text files; do not convert binary files. Add CRLF
+exceptions only when a tool demonstrably requires them. `git add --renormalize`
+normalizes the index but does not rewrite every working-copy file. If normalizing
+existing files, rewrite text files safely and verify that their content is unchanged
+apart from line endings; avoid destructive checkout/reset operations.
diff --git a/ChessRealms.ChessEngine.slnx b/ChessRealms.ChessEngine.slnx
new file mode 100644
index 0000000..bdeeae2
--- /dev/null
+++ b/ChessRealms.ChessEngine.slnx
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index f7bd507..935675e 100644
--- a/README.md
+++ b/README.md
@@ -8,25 +8,28 @@ dependencies. All six projects target .NET 10.
Install the stable [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
version **10.0.401**, as pinned in `global.json`. `rollForward: disable` requires
this exact SDK; `allowPrerelease: false` excludes previews. CI reads the same file.
+Install PowerShell 7 (`pwsh`) and Git for the solution structure check. See
+[CONTRIBUTING.md](CONTRIBUTING.md) for repository layout and contribution rules.
Run from the repository root:
```sh
dotnet --version
-dotnet restore src/ChessRealms.ChessEngine.sln --locked-mode
-dotnet build src/ChessRealms.ChessEngine.sln --configuration Release --no-restore
-dotnet test src/ChessRealms.ChessEngine.sln --configuration Release --no-build --filter "TestCategory!=Deep"
+pwsh -NoProfile -File scripts/Verify-SolutionStructure.ps1
+dotnet restore ChessRealms.ChessEngine.slnx --locked-mode
+dotnet build ChessRealms.ChessEngine.slnx --configuration Release --no-restore
+dotnet test ChessRealms.ChessEngine.slnx --configuration Release --no-build --filter "TestCategory!=Deep"
```
-For the ordinary developer loop, `cd src` and run `dotnet test`. This builds and
-runs all ordinary tests, including fast perft; no settings file is required.
-The solution lives in `src`, so commands from the repository root need its path.
+For the ordinary developer loop, run `dotnet test` from the repository root.
+This builds and runs all ordinary tests, including fast perft; no settings file is required.
+The solution lives at the repository root; all six projects remain in `src`.
Tests continue to use NUnit 3 through VSTest, explicitly selected in `global.json`.
The existing filters and NUnit `Explicit` behavior are unchanged.
Committed `packages.lock.json` files pin direct and transitive package versions
and content hashes. CI uses `--locked-mode` to reject dependency drift. When
intentionally updating packages, run
-`dotnet restore src/ChessRealms.ChessEngine.sln --force-evaluate`, review the
+`dotnet restore ChessRealms.ChessEngine.slnx --force-evaluate`, review the
lock-file changes, and repeat the checks above. When updating the SDK, update
`global.json` and this README together, then regenerate/review the lock files with
that SDK. Restore requires access to NuGet.org or a cache containing the locked packages.
@@ -43,21 +46,22 @@ that SDK. Restore requires access to NuGet.org or a cache containing the locked
opt-in for cost, not failing tests being suppressed. Select them explicitly:
```sh
-dotnet test src/ChessRealms.ChessEngine.sln --configuration Release --no-build --filter "TestCategory=Deep"
+dotnet test ChessRealms.ChessEngine.slnx --configuration Release --no-build --filter "TestCategory=Deep"
```
Run both test commands after the Release build, which compiles all tests. Tests,
the console perft runner and benchmarks use the same `PerftDriver` implementation.
-GitHub Actions (`.github/workflows/ci.yml`) runs restore, Release build and the
-same fast-test command on Windows and Linux with SDK 10.0.401, on pushes and pull requests.
+GitHub Actions (`.github/workflows/ci.yml`) checks solution structure, then runs
+locked restore, Release build and the same fast-test command on Windows and Linux
+with SDK 10.0.401, on pushes and pull requests.
Deep tests are not part of the default CI job. A local pass does not establish
that either GitHub Actions job has passed.
Optional coverage check using the existing VSTest collector:
```sh
-dotnet test src/ChessRealms.ChessEngine.sln -c Release --no-build --filter "TestCategory!=Deep" --collect:"XPlat Code Coverage"
+dotnet test ChessRealms.ChessEngine.slnx -c Release --no-build --filter "TestCategory!=Deep" --collect:"XPlat Code Coverage"
```
### Tool smoke checks
diff --git a/docs/dotnet-10-migration.md b/docs/dotnet-10-migration.md
index 5446fd0..b9c3e23 100644
--- a/docs/dotnet-10-migration.md
+++ b/docs/dotnet-10-migration.md
@@ -1,5 +1,10 @@
# .NET 10 migration
+> Repository organization note: the solution now lives at the repository root
+> as `ChessRealms.ChessEngine.slnx`.
+> Commands and results below preserve the paths used at the time of validation.
+> See [CONTRIBUTING.md](../CONTRIBUTING.md) for current commands.
+
Validated on 2026-09-10, Windows 11 x64. The starting commit was `d8eb1e3`
(`test: add regression test foundation and .NET 8 CI`), confirmed as `origin/main`
after fetching. The working tree was clean; no applicable `AGENTS.md` was found.
diff --git a/docs/game-rules-validation.md b/docs/game-rules-validation.md
index ead99a4..32ce7d5 100644
--- a/docs/game-rules-validation.md
+++ b/docs/game-rules-validation.md
@@ -1,5 +1,10 @@
# Game-rules validation — 2026-09-10
+> Repository organization note: the solution now lives at the repository root
+> as `ChessRealms.ChessEngine.slnx`.
+> Commands and results below preserve the paths used at the time of validation.
+> See [CONTRIBUTING.md](../CONTRIBUTING.md) for current commands.
+
Base: `origin/main` at `49fc3a5` (`.NET 10` migration), containing `d8eb1e3`
(restored tests and CI). Work branch: `feature/complete-game-rules`.
No applicable AGENTS.md was present in the repository or ancestor directories.
diff --git a/scripts/Verify-SolutionStructure.ps1 b/scripts/Verify-SolutionStructure.ps1
new file mode 100644
index 0000000..7b12047
--- /dev/null
+++ b/scripts/Verify-SolutionStructure.ps1
@@ -0,0 +1,215 @@
+#requires -Version 7.0
+<#
+Checks the SLNX solution against tracked and untracked, nonignored repository files.
+Run with: pwsh -NoProfile -File scripts/Verify-SolutionStructure.ps1
+No files are changed. Git must be available on PATH.
+#>
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
+$solutionPath = Join-Path $repositoryRoot 'ChessRealms.ChessEngine.slnx'
+$issues = [Collections.Generic.List[string]]::new()
+
+function Normalize-RelativePath([string] $Path) {
+ # Normalize separators only: spelling and hierarchy must remain exact.
+ return $Path.Replace('\', '/')
+}
+
+function Get-ParentPath([string] $Path) {
+ $separator = $Path.LastIndexOf('/')
+ if ($separator -lt 0) { return '' }
+ return $Path.Substring(0, $separator)
+}
+
+function Get-PhysicalPath([string] $Path) {
+ if ($Path -match '^[/\\]|^[A-Za-z]:' -or
+ @($Path.Split('/') | Where-Object { $_ -in @('', '.', '..') }).Count -gt 0) {
+ $issues.Add("Invalid repository-relative path: '$Path'.")
+ return $null
+ }
+
+ # Test-Path alone cannot detect casing errors on a case-insensitive volume.
+ $current = $repositoryRoot
+ $parts = [Collections.Generic.List[string]]::new()
+ foreach ($part in $Path.Split('/')) {
+ if (-not [IO.Directory]::Exists($current)) { return $null }
+ $entries = @([IO.Directory]::EnumerateFileSystemEntries($current))
+ $entry = @($entries | Where-Object { [IO.Path]::GetFileName($_) -ceq $part })
+ if ($entry.Count -eq 0) {
+ $entry = @($entries | Where-Object { [IO.Path]::GetFileName($_) -ieq $part })
+ }
+ if ($entry.Count -ne 1) { return $null }
+ $current = $entry[0]
+ $parts.Add([IO.Path]::GetFileName($current))
+ }
+ if (-not [IO.File]::Exists($current)) { return $null }
+ return $parts -join '/'
+}
+
+function Add-FolderAncestors([string] $Path) {
+ while ($Path) {
+ [void] $expectedFolders.Add($Path)
+ $Path = Get-ParentPath $Path
+ }
+}
+
+function Compare-References($Expected, $Actual, [string] $Kind) {
+ $seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
+ foreach ($reference in $Actual) {
+ $path = $reference.Path
+ if (-not $seen.Add($path)) { $issues.Add("Duplicate ${Kind}: '$path'.") }
+ if (-not $Expected.ContainsKey($path)) {
+ $caseMatch = @($Expected.Keys | Where-Object { $_ -ieq $path })
+ if ($caseMatch.Count -gt 0) {
+ $issues.Add("Case mismatch in ${Kind}: '$path'; expected '$($caseMatch -join "', '")'.")
+ } else {
+ $issues.Add("Extra ${Kind}: '$path'.")
+ }
+ } elseif ($reference.Folder -cne $Expected[$path]) {
+ $issues.Add("Misplaced ${Kind}: '$path' is in '$($reference.Folder)'; expected '$($Expected[$path])'.")
+ }
+ $physicalPath = Get-PhysicalPath $path
+ if ($null -eq $physicalPath) {
+ $issues.Add("Nonexistent file for ${Kind}: '$path'.")
+ } elseif ($physicalPath -cne $path) {
+ $issues.Add("Physical path case mismatch: '$path'; on disk '$physicalPath'.")
+ }
+ }
+ foreach ($path in $Expected.Keys) {
+ if (@($Actual | Where-Object { $_.Path -ceq $path }).Count -eq 0) {
+ $issues.Add("Missing ${Kind}: '$path' in '$($Expected[$path])'.")
+ }
+ }
+}
+
+try {
+ if ([IO.File]::Exists((Join-Path $repositoryRoot 'ChessRealms.ChessEngine.sln'))) {
+ $issues.Add('Legacy .sln remains beside .slnx; keep a single root solution for CLI discovery.')
+ }
+ # NUL-delimited output preserves spaces, Unicode and Git's otherwise quoted names.
+ $git = [Diagnostics.ProcessStartInfo]::new('git')
+ $git.WorkingDirectory = $repositoryRoot
+ $git.UseShellExecute = $false
+ $git.RedirectStandardOutput = $true
+ $git.StandardOutputEncoding = [Text.UTF8Encoding]::new($false)
+ foreach ($argument in @('ls-files', '--cached', '--others', '--exclude-standard', '-z')) {
+ $git.ArgumentList.Add($argument)
+ }
+ $process = [Diagnostics.Process]::Start($git)
+ $output = $process.StandardOutput.ReadToEnd()
+ $process.WaitForExit()
+ $gitExitCode = $process.ExitCode
+ $process.Dispose()
+ if ($gitExitCode -ne 0) { throw "git ls-files failed (exit $gitExitCode). Run inside a Git checkout." }
+
+ $expectedItems = [Collections.Generic.Dictionary[string, string]]::new([StringComparer]::Ordinal)
+ $expectedProjects = [Collections.Generic.Dictionary[string, string]]::new([StringComparer]::Ordinal)
+ $expectedFolders = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
+ $gitPaths = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
+ foreach ($gitPath in $output.Split([char]0, [StringSplitOptions]::RemoveEmptyEntries)) {
+ if (-not $gitPaths.Add($gitPath)) { continue }
+ if ($gitPath -notmatch '^(docs|scripts)/' -and $gitPath -notmatch '^src/.*\.csproj$') { continue }
+ $path = Get-PhysicalPath $gitPath
+ if ($null -eq $path) {
+ $issues.Add("Repository file is missing on disk: '$gitPath' (still listed by Git).")
+ $path = $gitPath
+ } elseif ($path -cne $gitPath) {
+ $issues.Add("Git path case mismatch: '$gitPath'; on disk '$path'. Record case-only renames in Git.")
+ }
+ if ($path -cmatch '^(docs|scripts)/') {
+ $parent = Get-ParentPath $path
+ $expectedItems[$path] = $parent
+ } elseif ($path -cmatch '^src/' -and $path -imatch '\.csproj$') {
+ # A project represents its own directory. Mirror only grouping directories
+ # above it: src/group/Project/Project.csproj belongs to src/group.
+ $directory = Get-ParentPath $path
+ $parent = if ($directory -ceq 'src') { 'src' } else { Get-ParentPath $directory }
+ $expectedProjects[$path] = $parent
+ } else {
+ $issues.Add("Repository root folder case mismatch: '$path'; use docs, scripts or src.")
+ continue
+ }
+ Add-FolderAncestors $parent
+ }
+
+ # SLNX represents nested folders by absolute names such as /docs/guides/.
+ # XML Folder elements are siblings, and their Project/File children are direct.
+ $settings = [Xml.XmlReaderSettings]::new()
+ $settings.DtdProcessing = [Xml.DtdProcessing]::Prohibit
+ $settings.XmlResolver = $null
+ $reader = [Xml.XmlReader]::Create($solutionPath, $settings)
+ $document = [Xml.XmlDocument]::new()
+ $document.XmlResolver = $null
+ try { $document.Load($reader) } finally { $reader.Dispose() }
+ if ($document.DocumentElement.LocalName -cne 'Solution' -or $document.DocumentElement.NamespaceURI) {
+ throw 'Expected an SLNX root without an XML namespace.'
+ }
+
+ $items = [Collections.Generic.List[object]]::new()
+ $projectReferences = [Collections.Generic.List[object]]::new()
+ $actualFolders = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
+ $declaredFolders = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
+ $ids = [Collections.Generic.HashSet[guid]]::new()
+ foreach ($node in $document.SelectNodes('//*')) {
+ if ($node.HasAttribute('Id')) {
+ $id = [guid]::Empty
+ if (-not [guid]::TryParse($node.GetAttribute('Id'), [ref] $id) -or $id -eq [guid]::Empty) {
+ $issues.Add("Invalid solution item GUID: '$($node.GetAttribute('Id'))'.")
+ } elseif (-not $ids.Add($id)) {
+ $issues.Add("Duplicate project/folder GUID: $id.")
+ }
+ }
+ if ($node.LocalName -in @('Folder', 'Project', 'File') -and
+ ($node.LocalName -cnotin @('Folder', 'Project', 'File') -or $node.NamespaceURI)) {
+ $issues.Add("Invalid SLNX element name: '$($node.LocalName)'.")
+ }
+ }
+ foreach ($folder in $document.SelectNodes('//Folder')) {
+ if ($folder.ParentNode -ne $document.DocumentElement) {
+ $issues.Add('Folder elements must be direct children of Solution; use full names such as /docs/guides/.')
+ }
+ $name = Normalize-RelativePath $folder.GetAttribute('Name')
+ if ($name -notmatch '^/[^/]+(?:/[^/]+)*/$' -or
+ @($name.Trim('/').Split('/') | Where-Object { $_ -in @('.', '..') }).Count -gt 0) {
+ $issues.Add("Invalid solution folder path: '$name'. Expected /folder/ or /folder/subfolder/.")
+ }
+ $path = $name.Trim('/')
+ if (-not $declaredFolders.Add($path)) { $issues.Add("Duplicate solution folder: '$path'.") }
+ # SLNX permits implicit ancestors: /docs/guides/ also creates /docs/.
+ while ($path) {
+ [void] $actualFolders.Add($path)
+ $path = Get-ParentPath $path
+ }
+ }
+ foreach ($node in $document.SelectNodes('//File | //Project')) {
+ $parent = $node.ParentNode
+ $folder = ''
+ if ($parent.LocalName -ceq 'Folder') {
+ $folder = (Normalize-RelativePath $parent.GetAttribute('Name')).Trim('/')
+ } elseif ($node.LocalName -ceq 'File' -or $parent -ne $document.DocumentElement) {
+ $issues.Add("Invalid placement of SLNX $($node.LocalName) element: '$($node.GetAttribute('Path'))'.")
+ }
+ $path = Normalize-RelativePath $node.GetAttribute('Path')
+ $reference = [pscustomobject]@{ Path = $path; Folder = $folder }
+ if ($node.LocalName -ceq 'File') { $items.Add($reference) } else { $projectReferences.Add($reference) }
+ }
+ foreach ($path in $actualFolders) {
+ if (-not $expectedFolders.Contains($path)) { $issues.Add("Extra or incorrectly cased solution folder: '$path'.") }
+ }
+ foreach ($path in $expectedFolders) {
+ if (-not $actualFolders.Contains($path)) { $issues.Add("Missing solution folder: '$path'.") }
+ }
+ Compare-References $expectedItems $items 'solution item'
+ Compare-References $expectedProjects $projectReferences 'project reference'
+} catch {
+ $issues.Add("Verification failed: $($_.Exception.Message)")
+}
+
+if ($issues.Count -gt 0) {
+ Write-Output "Solution structure check failed ($($issues.Count) discrepancies):"
+ foreach ($issue in $issues | Sort-Object -Unique) { Write-Output " - $issue" }
+ exit 1
+}
+Write-Output "Solution structure verified: $($expectedProjects.Count) projects, $($expectedItems.Count) solution items, $($expectedFolders.Count) folders."
+exit 0
diff --git a/src/ChessRealms.ChessEngine.Benchmark/packages.lock.json b/src/ChessRealms.ChessEngine.Benchmark/packages.lock.json
index c2e0953..db7d648 100644
--- a/src/ChessRealms.ChessEngine.Benchmark/packages.lock.json
+++ b/src/ChessRealms.ChessEngine.Benchmark/packages.lock.json
@@ -177,4 +177,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine.Console/packages.lock.json b/src/ChessRealms.ChessEngine.Console/packages.lock.json
index 9b6a75e..3e128ec 100644
--- a/src/ChessRealms.ChessEngine.Console/packages.lock.json
+++ b/src/ChessRealms.ChessEngine.Console/packages.lock.json
@@ -7,4 +7,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine.Perft/packages.lock.json b/src/ChessRealms.ChessEngine.Perft/packages.lock.json
index 9b6a75e..3e128ec 100644
--- a/src/ChessRealms.ChessEngine.Perft/packages.lock.json
+++ b/src/ChessRealms.ChessEngine.Perft/packages.lock.json
@@ -7,4 +7,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine.Tests/packages.lock.json b/src/ChessRealms.ChessEngine.Tests/packages.lock.json
index 904b746..56a9dbb 100644
--- a/src/ChessRealms.ChessEngine.Tests/packages.lock.json
+++ b/src/ChessRealms.ChessEngine.Tests/packages.lock.json
@@ -87,4 +87,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine.sln b/src/ChessRealms.ChessEngine.sln
deleted file mode 100644
index badddee..0000000
--- a/src/ChessRealms.ChessEngine.sln
+++ /dev/null
@@ -1,63 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.9.34728.123
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChessRealms.MagicBruteforce", "ChessRealms.MagicBruteforce\ChessRealms.MagicBruteforce.csproj", "{5C55E3CA-2541-4EA4-B4B6-3C5AAE87FB9D}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ChessEngine", "ChessEngine", "{0785FE11-03DA-45AC-8704-A67DE5C4777C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChessRealms.ChessEngine", "ChessRealms.ChessEngine\ChessRealms.ChessEngine.csproj", "{E8906E4A-0DDD-45E8-8C0B-8FBB4B5D832A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChessRealms.ChessEngine.Tests", "ChessRealms.ChessEngine.Tests\ChessRealms.ChessEngine.Tests.csproj", "{530985CE-2BD3-437A-8C66-C50A98CEA318}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChessRealms.ChessEngine.Benchmark", "ChessRealms.ChessEngine.Benchmark\ChessRealms.ChessEngine.Benchmark.csproj", "{029E70A1-269B-4F1F-A132-60011E54392D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChessRealms.ChessEngine.Perft", "ChessRealms.ChessEngine.Perft\ChessRealms.ChessEngine.Perft.csproj", "{2C20D484-BE51-452B-A393-F8F0C969F72E}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChessRealms.ChessEngine.Console", "ChessRealms.ChessEngine.Console\ChessRealms.ChessEngine.Console.csproj", "{CED75849-6A1D-4150-879D-103DFCD5C547}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {5C55E3CA-2541-4EA4-B4B6-3C5AAE87FB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5C55E3CA-2541-4EA4-B4B6-3C5AAE87FB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5C55E3CA-2541-4EA4-B4B6-3C5AAE87FB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5C55E3CA-2541-4EA4-B4B6-3C5AAE87FB9D}.Release|Any CPU.Build.0 = Release|Any CPU
- {E8906E4A-0DDD-45E8-8C0B-8FBB4B5D832A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E8906E4A-0DDD-45E8-8C0B-8FBB4B5D832A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E8906E4A-0DDD-45E8-8C0B-8FBB4B5D832A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E8906E4A-0DDD-45E8-8C0B-8FBB4B5D832A}.Release|Any CPU.Build.0 = Release|Any CPU
- {530985CE-2BD3-437A-8C66-C50A98CEA318}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {530985CE-2BD3-437A-8C66-C50A98CEA318}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {530985CE-2BD3-437A-8C66-C50A98CEA318}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {530985CE-2BD3-437A-8C66-C50A98CEA318}.Release|Any CPU.Build.0 = Release|Any CPU
- {029E70A1-269B-4F1F-A132-60011E54392D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {029E70A1-269B-4F1F-A132-60011E54392D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {029E70A1-269B-4F1F-A132-60011E54392D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {029E70A1-269B-4F1F-A132-60011E54392D}.Release|Any CPU.Build.0 = Release|Any CPU
- {2C20D484-BE51-452B-A393-F8F0C969F72E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2C20D484-BE51-452B-A393-F8F0C969F72E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2C20D484-BE51-452B-A393-F8F0C969F72E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2C20D484-BE51-452B-A393-F8F0C969F72E}.Release|Any CPU.Build.0 = Release|Any CPU
- {CED75849-6A1D-4150-879D-103DFCD5C547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CED75849-6A1D-4150-879D-103DFCD5C547}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CED75849-6A1D-4150-879D-103DFCD5C547}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CED75849-6A1D-4150-879D-103DFCD5C547}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {E8906E4A-0DDD-45E8-8C0B-8FBB4B5D832A} = {0785FE11-03DA-45AC-8704-A67DE5C4777C}
- {530985CE-2BD3-437A-8C66-C50A98CEA318} = {0785FE11-03DA-45AC-8704-A67DE5C4777C}
- {029E70A1-269B-4F1F-A132-60011E54392D} = {0785FE11-03DA-45AC-8704-A67DE5C4777C}
- {2C20D484-BE51-452B-A393-F8F0C969F72E} = {0785FE11-03DA-45AC-8704-A67DE5C4777C}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {E65FD084-D2F8-4F38-93AC-24C1EB66CE8A}
- EndGlobalSection
-EndGlobal
diff --git a/src/ChessRealms.ChessEngine/Core/Constants/DefaultChessConstants.cs b/src/ChessRealms.ChessEngine/Core/Constants/DefaultChessConstants.cs
index be3bb06..b7251d5 100644
--- a/src/ChessRealms.ChessEngine/Core/Constants/DefaultChessConstants.cs
+++ b/src/ChessRealms.ChessEngine/Core/Constants/DefaultChessConstants.cs
@@ -200,4 +200,4 @@ public static bool IsValid(int square)
{
return square >= a1 && square <= h8;
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine/Core/Constants/SquareMapping.cs b/src/ChessRealms.ChessEngine/Core/Constants/SquareMapping.cs
index 2656557..b1bf086 100644
--- a/src/ChessRealms.ChessEngine/Core/Constants/SquareMapping.cs
+++ b/src/ChessRealms.ChessEngine/Core/Constants/SquareMapping.cs
@@ -35,4 +35,4 @@ internal static class SquareMapping
public const ulong DARK_SQUARES = 0xAA55AA55AA55AA55;
public const ulong ALL_SQUARES = LIGHT_SQUARES | DARK_SQUARES;
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine/PieceColor.cs b/src/ChessRealms.ChessEngine/PieceColor.cs
index 2a3cb85..4038ffb 100644
--- a/src/ChessRealms.ChessEngine/PieceColor.cs
+++ b/src/ChessRealms.ChessEngine/PieceColor.cs
@@ -17,4 +17,4 @@ public static bool IsBlackOrWhite(this PieceColor color)
{
return color == PieceColor.Black || color == PieceColor.White;
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.ChessEngine/packages.lock.json b/src/ChessRealms.ChessEngine/packages.lock.json
index 4a91a8c..b9dcfe1 100644
--- a/src/ChessRealms.ChessEngine/packages.lock.json
+++ b/src/ChessRealms.ChessEngine/packages.lock.json
@@ -3,4 +3,4 @@
"dependencies": {
"net10.0": {}
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.MagicBruteforce/Magic.cs b/src/ChessRealms.MagicBruteforce/Magic.cs
index 92bcce8..8a4aeb5 100644
--- a/src/ChessRealms.MagicBruteforce/Magic.cs
+++ b/src/ChessRealms.MagicBruteforce/Magic.cs
@@ -82,4 +82,4 @@ public static ulong FindMagic(int square, int iterations, bool isBishop, Random
return 0;
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.MagicBruteforce/Program.cs b/src/ChessRealms.MagicBruteforce/Program.cs
index 6569aad..e6cecc4 100644
--- a/src/ChessRealms.MagicBruteforce/Program.cs
+++ b/src/ChessRealms.MagicBruteforce/Program.cs
@@ -59,4 +59,4 @@ static void Exit(int code, string msg)
{
Console.Error.WriteLine(msg);
Environment.Exit(code);
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.MagicBruteforce/Properties/launchSettings.json b/src/ChessRealms.MagicBruteforce/Properties/launchSettings.json
index fcd6c78..189de57 100644
--- a/src/ChessRealms.MagicBruteforce/Properties/launchSettings.json
+++ b/src/ChessRealms.MagicBruteforce/Properties/launchSettings.json
@@ -5,4 +5,4 @@
"commandLineArgs": "\"-777\" 750000"
}
}
-}
\ No newline at end of file
+}
diff --git a/src/ChessRealms.MagicBruteforce/packages.lock.json b/src/ChessRealms.MagicBruteforce/packages.lock.json
index 9b6a75e..3e128ec 100644
--- a/src/ChessRealms.MagicBruteforce/packages.lock.json
+++ b/src/ChessRealms.MagicBruteforce/packages.lock.json
@@ -7,4 +7,4 @@
}
}
}
-}
\ No newline at end of file
+}