@@ -7,7 +7,7 @@ applyTo: "**/*.cs,**/tests/**,**/*Tests/**"
77> Accumulated hard-won insights from the Python → C# migration, test coverage sprints,
88> the 453-tweak restoration campaign, and the large-file splitting campaign.
99> These rules are ** as important as the coding standards** — they prevent recurring mistakes.
10- > Last updated: 2026-04-09 (v6.26 .0, C# 13 / .NET 10.0-windows, ~ 7,518 tweaks, 127 categories, 3,239 tests)
10+ > Last updated: 2026-04-09 (v6.27 .0, C# 13 / .NET 10.0-windows, ~ 7,518 tweaks, 127 categories, 3,259 tests)
1111
1212---
1313
@@ -1637,7 +1637,8 @@ Version history:
16371637| v6.24.0 | 8 | +80 | — (Phase 5.3 Accessibility remaining + Phase 5.5 Developer : MagnifierAdvanced/LiveCaptions/EyeControlSettings/VoiceAccessControl/WinDbgSettings/WSLAdvanced/GitCredManager/ContainerRuntime) |
16381638| v6.25.0 | 5 | +50 | — (Phase 5.4 Energy & Battery Management : BatterySaver/ChargingOptimization/StandbyStates/CPUPowerStates/DisplayPower; 7,479 tweaks, 127 categories, 175 modules) |
16391639| v6.26.0 | 3 | +39 | Sprint 688 — 3 new Office GP security modules (PolicyOfficeWord/PolicyOfficeExcel/PolicyOfficeOutlook) + PolicyWindowsSearch expanded 1→10 tweaks; deleted 3 empty stubs; 7,518 tweaks, 127 categories, 175 modules |
1640- **Current version**: v6.26.0 — 7,518 tweaks, 127 categories, 175 files (31 original + 144 extracted/split). Run full gap analysis on all three phases before creating any new module.
1640+ | v6.27.0 | 0 | 0 | — (Phase 6.1–6.3 : TweakHistory audit logging + ExportCsv, HealthScoreService.CategoryHealthScores, ConflictDetector.DetectRegistryConflicts + ConflictSeverity/RegistryConflict; fix .runsettings HangTimeout warning; +20 tests → 3,259 total) |
1641+ **Current version**: v6.27.0 — 7,518 tweaks, 127 categories, 175 files (31 original + 144 extracted/split). Run full gap analysis on all three phases before creating any new module.
16411642
16421643---
16431644
@@ -2066,3 +2067,212 @@ $destName = if (Test-Path (Join-Path $TweaksDir "$className.cs")) {
20662067**Important**: The class name in the file must remain exactly `internal static class Input`
20672068(not renamed). Only the filename gets the prefix. TweakEngine discovers classes by
20682069reflection on the namespace, not by filename, so filenames are arbitrary.
2070+
2071+ ---
2072+
2073+ # # Blame Data Collector `HangTimeout` Is Invalid in .NET SDK 10.0.201+
2074+
2075+ In `.NET SDK 10.0.201`, the `HangTimeout` attribute on `<CollectDumpOnTestSessionHang>` in
2076+ ` .runsettings` became an unrecognised configuration key. Leaving it produces two non-fatal
2077+ **test-runner warnings** (not CS compiler warnings) on every `dotnet test` invocation:
2078+
2079+ ```
2080+ Microsoft.TestPlatform.targets(48,5): warning Data collector 'Blame' message:
2081+ The blame parameter key specified HangTimeout is not valid. Ignoring this key..
2082+ Microsoft.TestPlatform.targets(48,5): warning Data collector 'Blame' message:
2083+ All tests finished running, Sequence file will not be generated.
2084+ ```
2085+
2086+ These are classified as `warning` in the `build succeeded with N warning(s)` summary, but
2087+ they are **NOT** subject to `TreatWarningsAsErrors=true` — that policy applies to the C#
2088+ compiler (`CS*`) and Roslyn analyzers, not to the `Microsoft.TestPlatform.targets` blame
2089+ collector messages. Tests still run and pass correctly.
2090+
2091+ **Fix**: Remove the `HangTimeout="30000"` attribute from `<CollectDumpOnTestSessionHang>`.
2092+ Use `TestSessionTimeout` in `<RunConfiguration>` as the primary hang-protection mechanism.
2093+
2094+ ```xml
2095+ <!-- ❌ BAD — produces warning in .NET SDK 10.0.201+ -->
2096+ <CollectDumpOnTestSessionHang DumpType="None" HangTimeout="30000" />
2097+
2098+ <!-- ✅ GOOD — remove HangTimeout; rely on TestSessionTimeout *) -->
2099+ <CollectDumpOnTestSessionHang DumpType="None" />
2100+ ```
2101+
2102+ ** Fixed in** : v6.27.0 — ` tests/.runsettings ` updated.
2103+
2104+ ---
2105+
2106+ ## Copilot Agent Terminal Stdout Capture — Check Output Repeatedly, Not Once
2107+
2108+ When ` dotnet build ` or ` dotnet test ` is run in an async terminal in a Copilot agent session,
2109+ the stdout output arrives in chunks and may initially appear blank. The build IS running; output
2110+ is just buffered.
2111+
2112+ ** Pattern that works** :
2113+
2114+ 1 . Run the command with ` mode=async ` (non-blocking)
2115+ 2 . Call ` get_terminal_output ` once after 5–10 seconds — may see blank or partial output
2116+ 3 . Wait a further 5–10 seconds and call ` get_terminal_output ` AGAIN
2117+ 4 . Continue until you see final lines like ` Build succeeded in Ns ` or ` Test Run Successful `
2118+
2119+ ** Do NOT** :
2120+ - Retry the same build command assuming it didn't start
2121+ - Open a new terminal and run the command again (causes a second build to start concurrently)
2122+ - Wait for ` stdout ` in a ` mode=sync ` call with a 60-second timeout — on OneDrive paths,
2123+ core library compilation alone (` CoreCompile ` ) takes ~ 54 seconds before any output appears
2124+
2125+ ** Rule** : A ` Build succeeded ` line WILL appear in ` get_terminal_output ` output eventually.
2126+ Poll 3–5 times at 5–10 second intervals rather than treating the first blank response as failure.
2127+
2128+ ** Symptom context** : This is a terminal ringbuffer/scroll artefact in the VS Code agent
2129+ surface. The underlying process is running; the Copilot tool only captures what fits in the
2130+ current view. The ` RegiLattice.Core CoreCompile ` step takes ~ 54 s, then ` Core.Tests CoreCompile `
2131+ takes ~ 30 s — total ~ 84–110 s before the ` Build succeeded ` summary line appears.
2132+
2133+ ---
2134+
2135+ ## New Service Methods: ` ExportCsv ` Is Sync, ` ExportToJsonAsync ` Is Async
2136+
2137+ When adding new export methods to Core services (e.g., ` TweakHistory ` , ` Analytics ` ,
2138+ ` Favorites ` ), follow the established naming and sync/async convention:
2139+
2140+ | Method | Pattern | Why |
2141+ | ----------------------- | -------- | ------------------------------------------------- |
2142+ | ` ExportToJsonAsync() ` | ` async ` | JSON serialization can be large; async I/O avoids UI freeze |
2143+ | ` ExportCsv() ` | ` sync ` | CSV is row-by-row; ` StringBuilder ` + ` File.WriteAllText ` is fast enough |
2144+ | ` Load() ` / ` Flush() ` | ` sync ` | Small local file; OneDrive sync is async at OS level |
2145+ | Any network or pack I/O | ` async ` | Always async for anything going over the wire |
2146+
2147+ ** Specifically for ` TweakHistory.ExportCsv ` ** : The method writes a 7-column CSV with header
2148+ ` Timestamp,TweakId,Action,Result,Username,MachineName,SessionId ` . It uses ` StringBuilder `
2149+ and ` File.WriteAllText(path, csv, Encoding.UTF8) ` . Making this ` async ` would require
2150+ ` await File.WriteAllTextAsync ` and would force callers to ` await ` unnecessarily for a
2151+ local file that is typically under 1 KB.
2152+
2153+ ** Rule** : Do NOT make ` ExportCsv ` async unless profiling shows it is a bottleneck.
2154+
2155+ ---
2156+
2157+ ## ` [JsonIgnore(Condition = WhenWritingDefault)] ` for Backward-Compatible DTO Expansion
2158+
2159+ When adding optional fields to a serialised data class (e.g., ` HistoryEntry ` , ` AnalyticsEntry ` ),
2160+ use ` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] ` on nullable fields.
2161+ This preserves backward compatibility with existing JSON files that don't have the new fields:
2162+
2163+ ``` csharp
2164+ // ✅ GOOD — new field is omitted from JSON when null; old files deserialise correctly
2165+ [JsonPropertyName (" username" )]
2166+ [JsonIgnore (Condition = JsonIgnoreCondition .WhenWritingDefault )]
2167+ public string ? Username { get ; set ; }
2168+
2169+ // ❌ BAD — writes "username": null to every entry; bloats the file and breaks old parsers
2170+ [JsonPropertyName (" username" )]
2171+ public string ? Username { get ; set ; }
2172+ ```
2173+
2174+ ** Applied in** : ` TweakHistory.HistoryEntry ` — three new audit fields (` Username ` , ` MachineName ` ,
2175+ ` SessionId ` ) added in v6.27.0. Existing ` history.json ` files from v6.26.0 and earlier load
2176+ without error; new entries written by v6.27.0+ include the audit fields automatically.
2177+
2178+ ---
2179+
2180+ ## Per-Process Session ID Pattern — ` Guid.NewGuid().ToString("N")[..8] `
2181+
2182+ When you need a stable, short, readable ID that identifies "all operations in one process run"
2183+ (as opposed to a per-machine or per-user ID), use a static GUID truncated to hex:
2184+
2185+ ``` csharp
2186+ // ✅ GOOD — 8-char hex derived from a unique Guid; stable for the lifetime of the process
2187+ private static readonly string s_sessionId = Guid .NewGuid ().ToString (" N" )[.. 8 ];
2188+ ```
2189+
2190+ Properties:
2191+ - ** Length** : exactly 8 characters
2192+ - ** Charset** : lowercase hex ` [0-9a-f] ` (the ` "N" ` format omits hyphens)
2193+ - ** Uniqueness** : collision probability ~ 1.5 × 10⁻⁹ between two concurrent processes
2194+ - ** Stability** : same value for all operations in one process invocation
2195+ - ** Range notation** : ` [..8] ` = C# 13 range expression for ` Substring(0, 8) ` — requires .NET 8+
2196+
2197+ ** Applied in** : ` TweakHistory.TweakHistory ` — ` s_sessionId ` initialised once at class load,
2198+ stamped on every ` HistoryEntry ` recorded by that process run.
2199+
2200+ ** Test pattern** : To verify the contract, check both constraints in separate tests:
2201+
2202+ ``` csharp
2203+ [Fact ]
2204+ public void Record_SameProcess_SharesSessionId ()
2205+ {
2206+ var entry1 = .. .; var entry2 = .. .;
2207+ Assert .Equal (entry1 .SessionId , entry2 .SessionId ); // stable within process
2208+ }
2209+
2210+ [Fact ]
2211+ public void Record_SessionId_IsEightCharHex ()
2212+ {
2213+ var entry = .. .;
2214+ Assert .NotNull (entry .SessionId );
2215+ Assert .Equal (8 , entry .SessionId ! .Length );
2216+ Assert .Matches (@" ^[0-9a-f]{8}$" , entry .SessionId );
2217+ }
2218+ ```
2219+
2220+ ---
2221+
2222+ ## ` CategoryHealthScore ` Sealed Record — Placement After Class Closing Brace
2223+
2224+ When adding a companion record or struct to a service class in the same file, place it
2225+ ** after** the class closing brace, not inside the class:
2226+
2227+ ``` csharp
2228+ // ✅ GOOD — CategoryHealthScore is a sibling type in the same namespace, not nested
2229+ internal sealed class HealthScoreService { ... }
2230+
2231+ public sealed record CategoryHealthScore (
2232+ string Category , int Score , int AppliedCount , int TotalCount , string Recommendation );
2233+
2234+ // ❌ BAD — nested inside HealthScoreService; callers need the full qualified name
2235+ internal sealed class HealthScoreService {
2236+ public sealed record CategoryHealthScore (...);
2237+ }
2238+ ```
2239+
2240+ ** Why** : Callers (GUI, CLI, tests) need ` CategoryHealthScore ` without knowing which service
2241+ class hosts it. Placing it at namespace level ("sibling type") lets callers import it with
2242+ just ` using RegiLattice.Core.Services ` rather than ` using ... = HealthScoreService.CategoryHealthScore ` .
2243+
2244+ ---
2245+
2246+ ## ` ConflictSeverity ` Enum and ` RegistryConflict ` Record — Phase 6.3 Pattern
2247+
2248+ When extending ` ConflictDetector.cs ` or similar static analysis services:
2249+
2250+ 1 . Define supporting enums and record structs ** before** the service class declaration.
2251+ 2 . Use ` readonly record struct ` for result types that are small, value-typed, and returned
2252+ in ` IReadOnlyList<T> ` — avoids heap allocations in hot paths.
2253+ 3 . The ` DetectRegistryConflicts() ` method indexes all ` RegOp ` ApplyOps by ` "Path\Name" ` key.
2254+ Only ` SetValue ` -family ops (SetDword, SetString, etc.) are indexed — not Check, Delete, or DeleteTree ops.
2255+ 4 . Severity classification:
2256+ - ` Info ` — same path+name with the SAME value (redundant but not conflicting)
2257+ - ` Critical ` — same path+name with opposing binary values ` 0 ` / ` 1 ` (tweaks cancel each other)
2258+ - ` Warning ` — same path+name with different non-binary values (order-dependent behaviour)
2259+
2260+ ``` csharp
2261+ // Severity helper pattern:
2262+ private static ConflictSeverity ClassifySeverity (string valA , string valB )
2263+ {
2264+ if (string .Equals (valA , valB , StringComparison .Ordinal )) return ConflictSeverity .Info ;
2265+ if (int .TryParse (valA , out var ia ) && int .TryParse (valB , out var ib )
2266+ && ((ia == 0 && ib == 1 ) || (ia == 1 && ib == 0 )))
2267+ return ConflictSeverity .Critical ;
2268+ return ConflictSeverity .Warning ;
2269+ }
2270+ ```
2271+
2272+ ** Test pattern** : Use a private factory helper ` MakeSetDword(id, path, name, value) ` in the
2273+ test class to create minimal ` TweakDef ` instances without cluttering each test body:
2274+ ``` csharp
2275+ private static TweakDef MakeSetDword (string id , string path , string name , int value ) =>
2276+ new () { Id = id , Label = id , Category = " Test" ,
2277+ ApplyOps = [RegOp .SetDword (path , name , value )] };
2278+ ```
0 commit comments