# Copilot Instructions ## Project Details * .NET SDK pinned in #file:'global.json' * Common parameters specified in #file:'Directory.Build.props' * Central NuGet package version management – versions go in #file:'Directory.Packages.props', not in `.fsproj` files * Build: `dotnet build FSharp.Collections.Immutable.slnx` * Test: `dotnet test FSharp.Collections.Immutable.slnx` ## Solution Structure **IMPORTANT: Every collection binding implementation file must have a corresponding `.fsi` signature file.** The one exception is the `XTypes.fs` file of a folder, which holds only type abbreviations and is its own signature: a signature file without an implementation file does not compile (FS0240). The comparer struct and the extension files of a module carry `` in the project file, the `.fsi` on the module's `.fsi` and the `.fs` on its `.fs`, so that Visual Studio shows them as a tree under the module. ```text / ├── build/ – FAKE build scripts and release automation │ └── CheckOrder.fs – checks the category order of the signature files with the Fantomas parser ├── docsSrc/ – FSharp.Formatting documentation source ├── src/FSharp.Collections.Immutable/ – main library │ ├── Helper/ – internal helpers shared by the modules, one root module per file │ │ ├── ImmutableCollectionUtil.fs – argument checks and the wrapping of arrays │ │ ├── Comparers.fs – the default comparer of F# generic comparison │ │ ├── ErrorStrings.fs – the exception messages of FSharp.Core and the .NET libraries │ │ ├── Raise.fs – the exceptions of the FSharp.Core DetailedExceptions module │ │ ├── ArrayChecks.fs – the checks that the functions of several arrays share │ │ └── Arrays.fs – the algorithms over arrays that the default and Tuple modules share │ ├── FlatList/ – ImmutableArray API surface and builders │ │ ├── FlatListTypes.fs – the FlatList abbreviation │ │ ├── FlatList.fs / FlatList.fsi – includes the nested Parallel module │ │ ├── FlatListOption.fs / FlatListOption.fsi – Option namespace overrides │ │ ├── FlatListTuple.fs / FlatListTuple.fsi – Tuple namespace overrides │ │ ├── FlatListBuilder.fs / FlatListBuilder.fsi │ │ └── FlatListCE.fs / FlatListCE.fsi │ ├── IndexedList/ – ImmutableList API surface and builders │ │ ├── IndexedListTypes.fs │ │ ├── IndexedList.fs / IndexedList.fsi │ │ ├── IndexedListOption.fs / IndexedListOption.fsi │ │ ├── IndexedListTuple.fs / IndexedListTuple.fsi │ │ ├── IIndexedList.fs / IIndexedList.fsi │ │ ├── IIndexedListOption.fs / IIndexedListOption.fsi │ │ ├── IIndexedListTuple.fs / IIndexedListTuple.fsi │ │ ├── IndexedListBuilder.fs / IndexedListBuilder.fsi │ │ └── IndexedListCE.fs / IndexedListCE.fsi │ ├── Queue/ – ImmutableQueue API surface and CE │ │ ├── QueueTypes.fs – the Queue and IQueue abbreviations │ │ ├── Queue.fs / Queue.fsi │ │ ├── QueueOption.fs / QueueOption.fsi │ │ ├── QueueTuple.fs / QueueTuple.fsi │ │ ├── IQueue.fs / IQueue.fsi │ │ ├── IQueueOption.fs / IQueueOption.fsi │ │ ├── IQueueTuple.fs / IQueueTuple.fsi │ │ └── QueueCE.fs / QueueCE.fsi │ ├── Stack/ – ImmutableStack API surface and CE │ │ ├── StackTypes.fs – the Stack and IStack abbreviations │ │ ├── Stack.fs / Stack.fsi │ │ ├── StackOption.fs / StackOption.fsi │ │ ├── StackTuple.fs / StackTuple.fsi │ │ ├── IStack.fs / IStack.fsi │ │ ├── IStackOption.fs / IStackOption.fsi │ │ ├── IStackTuple.fs / IStackTuple.fsi │ │ └── StackCE.fs / StackCE.fsi │ ├── HashMap/ – ImmutableDictionary API surface and CE │ │ ├── HashMapTypes.fs – the HashMap and IMap abbreviations │ │ ├── HashMap.fs / HashMap.fsi – nested Tuple / ValueTuple submodules │ │ │ ├── HashMap.CustomComparers.fs / .fsi – creates hash maps with comparers │ │ │ └── HashMap.Extensions.fs / .fsi – ofArray / ofList / ofSeq for every binding shape │ │ ├── HashMapOption.fs / HashMapOption.fsi │ │ ├── HashMapTuple.fs / HashMapTuple.fsi │ │ ├── IMap.fs / IMap.fsi │ │ ├── IMapOption.fs / IMapOption.fsi │ │ ├── IMapTuple.fs / IMapTuple.fsi │ │ └── HashMapCE.fs / HashMapCE.fsi │ ├── SortedMap/ – ImmutableSortedDictionary API surface and CE │ │ ├── SortedMapTypes.fs │ │ ├── SortedMap.fs / SortedMap.fsi │ │ │ ├── SortedMap.CustomComparers.fs / .fsi │ │ │ └── SortedMap.Extensions.fs / .fsi │ │ ├── SortedMapOption.fs / SortedMapOption.fsi │ │ ├── SortedMapTuple.fs / SortedMapTuple.fsi │ │ └── SortedMapCE.fs / SortedMapCE.fsi │ ├── HashSet/ – ImmutableHashSet API surface and CE │ │ ├── HashSetTypes.fs │ │ ├── HashSet.fs / HashSet.fsi │ │ │ └── HashSet.CustomComparer.fs / .fsi – creates hash sets with a comparer │ │ ├── HashSetOption.fs / HashSetOption.fsi │ │ ├── HashSetTuple.fs / HashSetTuple.fsi │ │ ├── ISet.fs / ISet.fsi │ │ ├── ISetOption.fs / ISetOption.fsi │ │ ├── ISetTuple.fs / ISetTuple.fsi │ │ └── HashSetCE.fs / HashSetCE.fsi │ ├── SortedSet/ – ImmutableSortedSet API surface and CE │ │ ├── SortedSetTypes.fs │ │ ├── SortedSet.fs / SortedSet.fsi │ │ │ └── SortedSet.CustomComparer.fs / .fsi │ │ ├── SortedSetOption.fs / SortedSetOption.fsi │ │ ├── SortedSetTuple.fs / SortedSetTuple.fsi │ │ └── SortedSetCE.fs / SortedSetCE.fsi │ ├── FrozenMap/ – FrozenDictionary API surface and CE │ │ ├── FrozenMapTypes.fs │ │ ├── FrozenMap.fs / FrozenMap.fsi │ │ │ ├── FrozenMap.CustomComparers.fs / .fsi │ │ │ └── FrozenMap.Extensions.fs / .fsi │ │ ├── FrozenMapOption.fs / FrozenMapOption.fsi │ │ ├── FrozenMapTuple.fs / FrozenMapTuple.fsi │ │ └── FrozenMapCE.fs / FrozenMapCE.fsi │ ├── FrozenSet/ – FrozenSet API surface and CE (the BCL type keeps its name) │ │ ├── FrozenSet.fs / FrozenSet.fsi │ │ │ └── FrozenSet.CustomComparer.fs / .fsi │ │ ├── FrozenSetOption.fs / FrozenSetOption.fsi │ │ ├── FrozenSetTuple.fs / FrozenSetTuple.fsi │ │ └── FrozenSetCE.fs / FrozenSetCE.fsi │ └── ComputationExpressions.fs / .fsi – the computation expression instances └── tests/FSharp.Collections.Immutable.Tests/ – MSTest test project ├── TestCategories.fs – centralized custom test category attributes ├── TestCategoriesTests.fs – enforces the test category rules ├── Contracts/ – shared contract bases, inputs and assertion helpers └── / – ops records, contract classes, collection-specific, builder, computation expression, Option and Tuple tests ``` The modules follow ADR 0001 – Per-collection modules (`docsSrc/Explanations/ADR_0001_Collection_Modules.md`): * Each module groups its functions under banner comments `// ---- Category ----` in the order `Creation`, `Builder`, `Properties`, `Access`, `Search`, `Transformation`, `Folding`, `Partitioning and grouping`, `Sorting`, `Random`, `Conversion`, `Type-specific`, and sorts the functions of a category alphabetically (ordinal), active patterns last. The `.fs` file repeats the order of its `.fsi` file. `build/CheckOrder.fs` (FAKE target `CheckOrder`) enforces it. * A file with one module declares it as the root module of the file (`module FSharp.Collections.Immutable.FlatList`); the type abbreviations of a folder live in its `XTypes.fs`. No module carries `[]`. * `try*` functions return `voption`, tuples are struct tuples. The namespaces `FSharp.Collections.Immutable.Option` and `FSharp.Collections.Immutable.Tuple` repeat these functions with `option` results and reference tuples. * No module function creates a collection with comparers: the `CustomComparers*` (maps) and `CustomComparer*` (sets) structs do, and the modules create the structs with `forComparers`, `forKeyComparer` and `forComparer`. * Maps take bindings in every shape: `ofArray`, `ofList` and `ofSeq` are static extension members of `XExtensions`, overloaded for `KeyValuePair`, reference tuples and struct tuples; `addRange` and `setItems` take `KeyValuePair`s, and the nested `Tuple` and `ValueTuple` submodules of the module take the tuple shapes. * A function throws what the FSharp.Core function of the mirrored module throws for the same input: `Array` for `FlatList`, `List` for the other sequence-like modules, `Map` and `Set` for maps and sets. * Contract test bases in `Contracts/` take a record of the module's functions; a collection derives one test class per capability that it supports. Expected values and exceptions come from FSharp.Core. The categories are attributes on the test classes; test methods carry none. ## Libraries in Use * [`System.Collections.Immutable`](https://www.nuget.org/packages/System.Collections.Immutable/) – immutable collections foundation * [`FSharp.Core`](https://www.nuget.org/packages/FSharp.Core/) – F# language core library * [`MSTest`](https://github.com/microsoft/testfx) – test framework * [`Unquote`](https://github.com/SwensenSoftware/unquote) – expressive assertions for complex checks * [`FAKE`](https://fake.build/) – build and release scripting * [`Argu`](https://github.com/fsprojects/Argu) – command-line parsing for build tooling Use GitHub MCP tools for code search in these repositories when needed. ## MCP Servers Agents discover servers from #file:'.mcp.json'; these are only hints on when to prefer one. * For F# work, prefer the `F#` server ([FsLangMCP](https://github.com/Neftedollar/FsLangMCP)) over `rg`/plain text search whenever the task depends on symbol meaning, compile context, cross-project usage, diagnostics, or safe refactoring preview. Its tool package is pinned in #file:'.config/dotnet-tools.json' – run `dotnet tool restore` before first use. * Use the `GitHub` server for code search in dependency repositories. In Claude Code it authenticates through `.claude/scripts/github-mcp-headers.ps1`, which reuses the GitHub token stored by Git Credential Manager, because the server does not support the OAuth dynamic client registration Claude Code needs. * Use the `Microsoft Docs` server for official Microsoft and Azure documentation. * When adding or changing a server, keep `servers` (VS Code / GitHub Copilot) and `mcpServers` (Claude Code) in sync and sorted alphabetically by key. ## F# Coding Guidelines ### Language and Tooling * Always use the latest F# 10 features over old syntax. * If you are running outside of an IDE, or the IDE does not provide F# semantic tools, use FsLangMCP as the primary tool for F# code navigation, symbol discovery, diagnostics, usage search and refactoring preview. Prefer its semantic tools over plain text search. * The compiler generates `IsCaseName` instance properties (for example `IsOk`, `IsNotFound`) for each DU case – use them when a single-case check is needed. * Write at most one level of a type in the postfix ML style: `int[] seq`, `int voption[]`, `seq` – never `int array seq` or `int list list`. Only the postfix levels count: `seq` and `struct (int array * int array)` have one each. ### Asynchrony and Cancellation * Prefer `task` CE over `async` CE. * When a method must return non-generic `Task`, annotate the return type explicitly: `member _.MyMethod (...) : Task = task { ... }`. Never cast through pipelines. * `task` CE can await `ValueTask` APIs directly. * Curried functions take the `CancellationToken` as their **first** parameter, so that it can be partially applied. * Never hand-roll wrappers such as `ValueTask (task { ... })`, `.AsTask ()` round-trips or manual `unit -> Task` thunks. Use the matching [`IcedTasks`](https://github.com/TheAngryByrd/IcedTasks) CE instead (`valueTask`, `valueTaskUnit`, `taskUnit`, `coldTask`, `cancellableTask`, `cancellableValueTask`, `backgroundTask`, …) and always use the full CE names, not the short aliases (`vTask`, `pvTask`, …). ### Values and Collections * Prefer `voption` (`ValueSome`/`ValueNone`) over `option`. Fields, members, parameters and values shared between threads included: none of them is a reason to pick `option`. Exception: when an API hands you `'T option` and has no `voption` counterpart, unwrap it with `Option.defaultValue`/`Option.defaultWith` directly – do not insert `ValueOption.ofOption` just to switch modules. * The mirror case, an API that *takes* `'T option` (an optional argument `?name = …`, a field typed `'T option`): stay in `ValueOption` through the whole chain and convert once, last – `x |> ValueOption.bind _.Value |> ValueOption.toOption`, never `x |> ValueOption.toOption |> Option.bind _.Value`. * Prefer `struct ('T1 * 'T2)` over reference tuples, and anonymous struct records (`struct {| ... |}`) over tuples for return types of public functions and methods. * Never group with `Seq.groupBy` – use `ToLookup` from `System.Linq`. It groups once into an `ILookup<'Key, 'T>` instead of re-grouping on every enumeration and does not allocate a tuple per group. Pass a lambda (`xs.ToLookup (fun x -> keyOf x)`), not a bare function value. * When casting sequence items use `Seq.cast` instead of `Seq.map (fun item -> item :> TargetType)`. * When concatenating two sequences or lists, prefer `seq { yield! xs; yield! ys }` (or `[ yield! xs; yield! ys ]` for lists) over the `@` operator or `Seq.append`. * When pipe operators are used on a materializable collection multiple times in a row, prefer `Seq` module for the chain and materialize at the end. * Keep a pipeline in harmony: when consecutive steps can come from one module, take them from that module, the conversion at the end included – `xs |> Seq.filter isValid |> Seq.sort |> Seq.toArray`, not `… |> Seq.sort |> Array.ofSeq`, and `items |> List.map _.Id |> List.toArray`, not `… |> Array.ofList`. Change the module only where the steps that follow need the other collection. * Compare `System.Type` values with `Type.(=) (a, b)`, the `Type.op_Equality` operator. The F# `=` goes through generic equality and is several times slower, and comparing names is both slow and wrong: `FullName.StartsWith` is culture-sensitive without a `StringComparison`, and the full name of an array of a generic type, such as ``FSharpOption`1[[System.Int32, ...]][]``, starts with the name of the generic type. To recognize a generic type, check `t.IsGenericType` and compare `t.GetGenericTypeDefinition ()` with its definition, such as `typedefof<_ option>`, through `Type.(=)`. ### Functions, Lambdas and Strings * Prefer underscore lambda syntax like `Seq.map _.Name` over `Seq.map (fun x -> x.Name)`, but only when the expression is a simple member access. Complex expressions like `Seq.where (fun x -> x.Name = name)` or `Seq.map (fun x -> x.Field1, x.Field2)` cannot be simplified. A member chain ending in a method call is not complex: `_.Changed.Subscribe(handler)`. The shorthand needs its input type known, so pipe the value in first – `value |> ValueOption.map _.Id`, not `ValueOption.map _.Id value` (FS0072). Never write a space in `_.MethodCall()` – it breaks parsing. * Simplify `Seq.map (fun x -> someFunction x)` to `Seq.map someFunction`. * Prefer interpolated strings over `printf` functions for string formatting. Format specifiers like `$"%s{value}"` are valid in interpolated strings and help type inference. * Pass an explicit `StringComparison` to every `Equals`, `StartsWith`, `EndsWith`, `IndexOf`, `Contains` and `Compare`, and an explicit comparer to every `HashSet` and `Dictionary`. `Ordinal` by default; culture-sensitive comparison is a decision, never a default. Use `OrdinalIgnoreCase` only where the thing compared really is case-insensitive. * A slice of a string that is only inspected – compared, trimmed, scanned, matched against a prefix – is a `ReadOnlySpan` (`text.AsSpan (start, length)`), not a `Substring`: the substring allocates a copy per call, the span does not. Materialize with `Substring`/`ToString ()` only for the value that leaves the function or is stored. * The `StringComparison` rule applies to spans unchanged: `MemoryExtensions` has `StringComparison` overloads of `StartsWith`, `EndsWith`, `Equals`, `CompareTo`, `IndexOf` and `Contains` for `ReadOnlySpan` – `s.AsSpan().TrimStart(' ').StartsWith("//", StringComparison.Ordinal)`. The overloads without one are the generic element-wise `ReadOnlySpan<'T>` ones – ordinal for `char` by accident, not by statement. * A slice that must be stored – kept in a record or class field, captured by a closure or used across a `let!`/`do!` in a `task` CE, put into a tuple, option or collection – is `ReadOnlyMemory` (`text.AsMemory (...)`), never a `ReadOnlySpan`: a byref-like value cannot be stored. Returning a `ReadOnlySpan` sliced from a string or from a span parameter is fine. * To compare two slices without building either, use `String.CompareOrdinal (a, aIndex, b, bIndex, length)` or `spanA.Equals (spanB, StringComparison.Ordinal)`. ### Nullable Reference Types * Declare variables non-nullable; check for `null` at entry points only. * Trust the SDK null annotations – do not add null checks when the type system says a value cannot be null. * Use `withNull` for null checks instead of boxing delegates/functions (avoid `isNull (box value)`). * Prefer `match` on `null` over `if isNull` – it narrows the type and suppresses nullness warnings: ```fsharp // Preferred match someObject with | null -> () | someObject -> someObject.SomeProperty ``` * Before suppressing a nullness warning (3261, 3262, …), exhaust these alternatives in order: 1. `nonNull value` – asserts non-null at runtime and fails fast. 2. `Unchecked.nonNull value` – skips the runtime check; only when non-null was already verified upstream. 3. An inline `#nowarn` / `#warnon` pair around the smallest possible scope – last resort for interop boundaries, centralised in a single helper rather than scattered across call sites. * Never suppress warnings file-wide; `#nowarn` and `#warnon` are valid anywhere in a file. ### XML Documentation Comments * A doc comment is either plain text with no tags at all, or fully explicit XML starting with `` – never a mixture. The compiler adds `` by itself only when the comment has no tags, and silently escapes tags otherwise: * No tags anywhere → bare `///` lines, no ``. * Any tag at all (``, ``, ``, ``, ``, …) → the comment must start with an explicit ``, and every `` must be inside it. ```fsharp // ✅ plain text — the compiler supplies /// Represents the result of a read operation. // ❌ a sibling tag without — the is escaped into the summary and lost /// Reads an item. /// Item Id // ✅ any tag present, so the comment is explicit XML throughout /// Reads an item. /// Item Id ``` * Keep `` and `` on one line each only when both are short – up to 60 characters of text each. When either is longer, write both over several lines, with each opening and closing tag on a line of its own: ```fsharp /// Returns the number of elements. /// This is an O(1) operation. /// /// The inputs of the set contracts as rows of DynamicData: none, one and several elements. /// /// /// The property builds new arrays on each read, so that no test sees a row that another one changed. /// ``` * Every public API must have XML documentation. * On an explicit interface implementation (`interface X with member _.M (...) = ...`), write `/// ` alone instead of restating the interface member's documentation, unless this implementation has behavior worth calling out beyond what the interface already documents – write a normal ``/`` there instead. * Refer to types and members through ``, never through `` or plain text. `` is for literal values only (JSON, SQL, setting names). Refer to language keywords through ``. * Split multi-paragraph documentation into `` elements inside `` – bare line breaks are collapsed by documentation renderers. * Write enumerations as `` (or `type="number"`) with `…`, never as Markdown-style bullets. ### Opens Sorting Sort `open` statements alphabetically within groups: `System` first, `Microsoft` second, `FSharp` third; then other external namespaces; then this solution's namespaces. `open type` goes last in each group. Type and module aliases form a separate final group. ### Class Constructors This is how to define a non-default F# class constructor: ```fsharp type DerivedClass = inherit BaseClass new (``arguments here``) as ``created object`` = // create any objects used in the base class constructor let fieldValue = "" { inherit BaseClass (``arguments here``) } then ``created object``.otherField <- fieldValue [] val mutable otherField : FieldType ``` ### Class Instantiation Always prefer F# class initializers over property assignment! **You absolutely must use F# class initializers instead of property assignment**! Class declaration: ```fsharp type MyClass (someConstructorParam : string) = member ReadOnlyProperty = someConstructorParam member val MutableProperty1 = "" with get, set member val MutableProperty2 = "" with get, set ``` Wrong: ```fsharp let myClass = MyClass("some value") myClass.MutableProperty1 <- "new value" myClass.MutableProperty2 <- "new value" ``` Right: ```fsharp let myClass = MyClass( // constructor parameters go first without names "some value", // then mutable properties go next with names MutableProperty1 = "new value", MutableProperty2 = // operations must be placed into parentheses (5 |> string) ) ``` ### C#-Consumable Extension Members ```fsharp // AutoOpen makes the module automatically available without an explicit open statement // Extension makes the members visible to C# [] module MyTypeExtensions = type MyType with // Extension is visible to C# // CompiledName makes the method name friendly to C# [] member this.ExtensionMethod (param1 : string) : ReturnType = () ``` ## Naming Conventions * Use PascalCase for modules, types, and public members. * Use camelCase for `let` bindings, functions, private fields, and local variables. * Prefix interface names with `I`. * Do not prefix type parameters with `T` (e.g., use `'Result` instead of `'TResult`). * Name tests using spaces (e.g., `member this.``Test name with spaces``() : Task = ...`). ## Testing * Tests use MSTest 4. * Collections in tests are arrays: build and inspect them with `toArray` and `[| ... |]`, and compare them with `CollectionAssert.AreEqual`. Arrays are faster to build than F# lists, and `CollectionAssert` compares them element by element, nested arrays included; it cannot work with F# lists. Convert to a list (`toList`, `List.ofSeq`) only to pattern-match on it or to call the `List` function that the tested function mirrors; a list that is an input of the tested function (`ofList`, a list given to `ofSeq`) is a case of its own. * Never compare arrays with `Assert.AreEqual` – it compares references. * `CollectionAssert` compares tuple elements with `Equals`, so an array inside a tuple is compared by reference; check such results with Unquote, where `=` compares the array structurally. * Give the cases of a test as data rows and parameters of the test method instead of a loop over a list of cases inside the test: `[]` for constants (`[]`), `[]` for rows that are computed or shared by several tests. Each row runs and reports as a separate test named after its arguments: a failing case does not stop the others, and the message does not need to repeat the input. * A `DynamicData` source is a public static property that builds its rows on each read (`static member Rows = ...`, not `static member val`, whose rows every class that inherits the test would share), and a test never changes its row. * `typeof<_>` cannot name an F# module, so the sources that several test classes share go into a static class (`[]`): `DynamicData (nameof TestData.Rows, typeof)`. The sources of one class, a generic contract class included, are its own static members: `DynamicData (nameof MyContract<'C>.Rows)`. * A row is a single value, a tuple, which is unpacked into the parameters of the method, or a `TestDataRow<'T>`. Nested loops become one source: `seq { for index in ... do for count in ... do struct (index, count) }`. * MSTest names a row by the `ToString` of its values, so a row holds printable data – numbers, strings, arrays of them, F# union cases – and the test builds the collections and picks the functions from it. A collection or a function in a row gives a name like ``ImmutableHashSet`1[System.String]``. * Wrap a row that is an array of a reference type (`string array`, an array of reference tuples) in `TestDataRow`: MSTest spreads such an array into the parameters, as it does an `obj array`. * Do not set `TestDataRow.DisplayName`: it replaces the whole name of the test, the name of the method included. * `StringAssert` has overloads with `StringComparison`. * Use `Assert.Contains` instead of `Assert.IsTrue (str.Contains ..., "message")`, and do not put the actual value into the message. * Check collection size with `Assert.HasCount (expected, collection, "message")` instead of `Assert.AreEqual (expected, collection.Length, "message")` (or `.Count` / `Seq.length`); use `Assert.IsEmpty`, `Assert.IsNotEmpty` and `Assert.ContainsSingle` for the zero, non-zero and single-item cases. On failure they report the actual count and items. * Try running tests with `--no-build` first, run them individually where possible, and use the trx format for results so failures can be consumed and fixed. * Use Unquote only for complex object/hierarchy assertions; for simple scalar checks prefer standard `Assert.*` APIs. * Every `Assert.*` call **must include a failure message** so output is self-explanatory. * Async tests must return `Task`, not `Async` or `Task` – always declare `) : Task = task {`. ## General * Make only high-confidence suggestions when reviewing code changes. * Write code with good maintainability practices, including comments on why certain design decisions were made. * Handle edge cases and write clear exception handling. * Never duplicate code unless explicitly allowed. * All comments, documentation, README files, and markdown files must be written in **English only**.