Skip to content

Commit e00ff1d

Browse files
committed
feat(services): Phase 6.1-6.3 — audit logging, health scores, conflict detection, v6.27.0
- TweakHistory: Username/MachineName/SessionId audit fields + ExportCsv() (7-col CSV) - HealthScoreService: CategoryHealthScores() + CategoryHealthScore sealed record - ConflictDetector: DetectRegistryConflicts(), ConflictSeverity enum, RegistryConflict struct - tests/.runsettings: removed invalid HangTimeout attr (SDK 10.0.201+ breaking change) - lessons-learned: 7 new lessons (HangTimeout, terminal polling, ExportCsv sync, JsonIgnore, SessionId, CategoryHealthScore placement, Phase 6.3 pattern) - copilot-instructions + testing instructions: test count 3239 -> 3259 - All 28 metadata files bumped to v6.27.0 Tweaks: 7518 | Modules: 175 | Categories: 127 | Tests: 3259 (+20), 0 failures
1 parent 84a62a3 commit e00ff1d

26 files changed

Lines changed: 744 additions & 40 deletions

.github/copilot-instructions.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> Auto-loaded by GitHub Copilot on every chat/agent session in this workspace.
44
> Keep this file accurate — it is the fastest path to project understanding.
5-
> Last verified: 2026-04-09 (v6.26.0, ~7,518 tweaks, 127 categories, 3,239 tests).
5+
> Last verified: 2026-04-09 (v6.27.0, ~7,518 tweaks, 127 categories, 3,259 tests).
66
77
## Companion Instruction Files
88

@@ -79,12 +79,12 @@ Rules:
7979
| -------- | ------------------------------------------------------------------------ |
8080
| Language | C# 13 / .NET 10.0-windows (x64) |
8181
| Build | `dotnet build` / MSBuild via `RegiLattice.sln` |
82-
| Test | xUnit 2.9.3 — 3,239 tests (0 failures) |
82+
| Test | xUnit 2.9.3 — 3,259 tests (0 failures) |
8383
| GUI | WinForms with 11 themes (Catppuccin Mocha/Latte, Nord, Dracula + 7 more) |
84-
| Version | 6.26.0 |
84+
| Version | 6.27.0 |
8585
| Install | `dotnet build RegiLattice.sln -c Release` |
8686
| Tweaks | 7,518 across 127 categories (175 files) |
87-
| Tests | 3,239 passing (0 consistent failures) |
87+
| Tests | 3,259 passing (0 consistent failures) |
8888
| NuGet | System.Management 10.0.5, Microsoft.NET.Test.Sdk 17.14.1 |
8989

9090
## Build Quality Standards — Non-Negotiable
@@ -94,7 +94,7 @@ Rules:
9494
| Build fatals | **0** — hard CI fail |
9595
| Build errors | **0** — hard CI fail |
9696
| Build warnings | **0**`TreatWarningsAsErrors=true` in `Directory.Build.props` |
97-
| Test failures | **0** — all 3,239+ tests must pass |
97+
| Test failures | **0** — all 3,259+ tests must pass |
9898
| Skipped tests | **0**`[Fact(Skip=...)]` / `[Theory(Skip=...)]` forbidden |
9999
| Warning suppressions | **0**`#pragma warning disable` / `[SuppressMessage]` forbidden; fix at source |
100100
| TODO / FIXME comments | **0** — open a GitHub Issue instead; no inline deferrals |
@@ -143,7 +143,7 @@ git push; git push --tags # ← REQUIRED on every version bump
143143

144144
> Full annotated solution tree: see `.github/instructions/workspace.instructions.md` — Solution Structure section.
145145
146-
Key namespaces: `RegiLattice.Core` (engine + models + registry + tweak modules, 31 files), `RegiLattice.GUI` (WinForms, 11 themes), `RegiLattice.CLI` (25+ commands). Tests live in `tests/` — 3 projects, 3,239 total.
146+
Key namespaces: `RegiLattice.Core` (engine + models + registry + tweak modules, 31 files), `RegiLattice.GUI` (WinForms, 11 themes), `RegiLattice.CLI` (25+ commands). Tests live in `tests/` — 3 projects, 3,259 total.
147147

148148
### TweakDef Model
149149

@@ -392,7 +392,7 @@ Canonical category slugs:
392392

393393
> Full test file inventory and coverage targets: see `.github/instructions/testing.instructions.md` — Test File Structure section.
394394
395-
Projects: `RegiLattice.Core.Tests` (2,442 tests), `RegiLattice.CLI.Tests` (434 tests), `RegiLattice.GUI.Tests` (363 tests). Total: 3,239.
395+
Projects: `RegiLattice.Core.Tests` (2,462 tests), `RegiLattice.CLI.Tests` (434 tests), `RegiLattice.GUI.Tests` (363 tests). Total: 3,259.
396396

397397
## Adding a New Tweak — Checklist
398398

.github/instructions/lessons-learned.instructions.md

Lines changed: 212 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
20682069
reflection 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+
```

.github/instructions/testing.instructions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ applyTo: "**/tests/**,**/*Tests/**,**/*Tests.csproj,**/test_*.py,**/conftest.py"
1818
| `RegiLattice.Core.Tests` | 2,442+ | TweakDef, TweakEngine, RegistrySession, Services, Plugins, Locale, SnapshotManager, TweakValidator, DependencyResolver, Favorites, TweakHistory, ConfigExporter, SystemMonitor, BatchImpactEstimator |
1919
| `RegiLattice.CLI.Tests` | 434+ | CLI argument parsing (ParseArgs, CliArgs, ConsoleColorizer) |
2020
| `RegiLattice.GUI.Tests` | 363+ | Theme, PackageManagerValidation, PackageNameValidator, AppIcons |
21-
| **Total** | **3,239+**| |
21+
| **Total** | **3,259+**| |
2222

2323
## Running Tests
2424

@@ -237,7 +237,7 @@ Every test run and every CI build must meet these conditions before merging:
237237
| Build fatals | **0** — hard CI fail |
238238
| Build errors | **0** — hard CI fail |
239239
| Build warnings | **0**`TreatWarningsAsErrors=true`; any warning blocks CI |
240-
| Test failures | **0** — all 3,239+ tests must pass |
240+
| Test failures | **0** — all 3,259+ tests must pass |
241241
| Skipped tests | **0**`[Fact(Skip=...)]` and `[Theory(Skip=...)]` are forbidden |
242242
| Inline suppressions | **0**`#pragma warning disable`, `[SuppressMessage]`, `// NOSONAR`, `// NCA`, |
243243
| | `// ReSharper disable`, `// NOLINT` — all forbidden; fix root cause instead |

Directory.Build.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
Configuration Manager to the x64 platform. Both are required. -->
2020
<Platforms>x64</Platforms>
2121
<PlatformTarget>x64</PlatformTarget>
22-
<Version>6.26.0</Version>
23-
<AssemblyVersion>6.26.0.0</AssemblyVersion>
24-
<FileVersion>6.26.0.0</FileVersion>
25-
<InformationalVersion>6.26.0</InformationalVersion>
22+
<Version>6.27.0</Version>
23+
<AssemblyVersion>6.27.0.0</AssemblyVersion>
24+
<FileVersion>6.27.0.0</FileVersion>
25+
<InformationalVersion>6.27.0</InformationalVersion>
2626
<Deterministic>true</Deterministic>
2727
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
2828
<TieredCompilation>true</TieredCompilation>

0 commit comments

Comments
 (0)