Skip to content

Commit aff8289

Browse files
committed
docs: Sprint 8 CHANGELOG, README badge 17511, API.md new functions (phases 95-100)
CHANGELOG.md: - New [Unreleased] Sprint 8 section documenting C13-C23 additions: set_expand_string/read_expand_string, set_multi_sz/read_multi_sz, corp_guard_reasons, reset_corp_cache, detect_battery, detect_network_type, HWProfile.has_battery/network_type, analytics.record_error_for/error_stats, ratings.average_rating/rated_count, tweaks engine helpers (5 new functions), AppConfig.theme/locale, CLI --scope/--min-build/--corp-safe/--needs-admin, _split_root lru_cache, filter_tweaks early-exit, status_map detect-free skip README.md: - Test count badge: 17378 -> 17511 API.md: - tweaks: tweaks_by_scope, tweaks_above_build, tweak_risk_level, tweak_count_by_scope, category_counts - registry: set_expand_string, set_multi_sz, read_expand_string, read_multi_sz - config: theme and locale fields - corpguard: corp_guard_reasons, reset_corp_cache - analytics: record_error_for, error_stats - ratings: new section with average_rating, rated_count - CLI: --scope, --min-build, --corp-safe, --needs-admin flags Also: ruff auto-fix applied to test files (unused imports, sort)
1 parent bd2ead4 commit aff8289

6 files changed

Lines changed: 105 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,55 @@ All notable changes to RegiLattice are documented here.
44
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55
This project adheres to [Semantic Versioning](https://semver.org/).
66

7+
## [Unreleased] — Sprint 8 refactor (C13–C23, 2026-03)
8+
9+
### Added
10+
11+
- **`RegistrySession.set_expand_string` / `read_expand_string`** — read/write `REG_EXPAND_SZ` values with lru_cache.
12+
- **`RegistrySession.set_multi_sz` / `read_multi_sz`** — read/write `REG_MULTI_SZ` (list-of-strings) values with lru_cache.
13+
- **`corp_guard_reasons()`** — returns a copy of the reason-list that triggered corporate detection; thread-safe.
14+
- **`reset_corp_cache()`** — clears the corporate detection cache for testing and hot-reload scenarios.
15+
- **`detect_battery()`**`@lru_cache` probe returning `True` when a battery/UPS is detected.
16+
- **`detect_network_type()`**`@lru_cache` probe returning `"vpn"`, `"wifi"`, `"ethernet"`, or `"unknown"`.
17+
- **`HWProfile.has_battery`** and **`HWProfile.network_type`** fields populated by the new probes.
18+
- **`analytics.record_error_for(tweak_id)`** — track per-tweak error counts.
19+
- **`analytics.error_stats()`** — return `dict[str, int]` of per-tweak error counts.
20+
- **`ratings.average_rating()`** — mean star rating across all rated tweaks (`None` when empty).
21+
- **`ratings.rated_count()`** — number of tweaks that have been rated.
22+
- **`tweaks_by_scope(scope)`** — return tweaks matching a specific scope string.
23+
- **`tweaks_above_build(build)`** — return tweaks whose `min_build` is `<= build`.
24+
- **`tweak_risk_level(td)`** — classify a tweak as `"low"`, `"medium"`, or `"high"` risk.
25+
- **`tweak_count_by_scope()`**`dict[str, int]` counts per scope key (`user/machine/both`).
26+
- **`category_counts()`**`dict[str, int]` mapping category name to tweak count.
27+
- **`AppConfig.theme`** — new field (default `"system"`) loaded from `[general] theme` in TOML.
28+
- **`AppConfig.locale`** — new field (default `"en"`) loaded from `[general] locale` in TOML.
29+
- **CLI `--scope {user,machine,both}`** — filter `--list` / `--search` output by registry scope.
30+
- **CLI `--min-build N`** — filter `--list` / `--search` output by minimum Windows build.
31+
- **CLI `--corp-safe`** — filter `--list` / `--search` to HKCU-only tweaks.
32+
- **CLI `--needs-admin`** — filter `--list` / `--search` to admin-required tweaks.
33+
- **133 new tests** across 9 test files (C16–C23 additions).
34+
35+
### Changed / Fixed
36+
37+
- **`_invalidate_cache_for` bugfix** — previously only cleared `dword`/`string`/`exists` suffixes; now clears
38+
all 6: `dword`, `string`, `binary`, `qword`, `expand`, `multi_sz`.
39+
- **`filter_tweaks()` early-exit** — adds `if not pool: return pool` after each filter step, avoiding
40+
unnecessary work when the result pool drains early.
41+
- **`status_map()` detect-free skip** — tweaks with `detect_fn=None` are assigned `TweakResult.UNKNOWN`
42+
directly without being submitted to the thread pool.
43+
- **`_split_root()` memoization** — decorated with `@functools.lru_cache(maxsize=256)`; repeated registry
44+
path splitting is now O(1) on cache hit.
45+
- **`detect_hardware()` workers raised to 6** — runs `detect_battery` and `detect_network_type` in the
46+
parallel probe pool alongside the existing 4 probes.
47+
- **`_SCOPE_CACHE` / `_SCOPE_LOCK` ordering fix** — moved definitions to before the `_load_plugins()`
48+
function to eliminate `NameError` at import time.
49+
- **Scope pre-warm**`_load_plugins()` now pre-populates `_SCOPE_CACHE` for all tweaks at import.
50+
51+
### Infrastructure
52+
53+
- **17 511 tests** across 21 test files after C13–C23 additions (was 17 378 at Sprint 7).
54+
- ruff: all checks pass; mypy `--strict`: 0 issues.
55+
756
## [Unreleased] — Sprint 7 refactor (C2–C11, 2026-03)
857

958
### Added

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ⚡ RegiLattice
22

33
![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white)
4-
![Tests](https://img.shields.io/badge/tests-17%2C378%2B%20passing-brightgreen)
4+
![Tests](https://img.shields.io/badge/tests-17%2C511%2B%20passing-brightgreen)
55
![Coverage](https://img.shields.io/badge/coverage-90%25%2B-brightgreen)
66
![Ruff](https://img.shields.io/badge/linter-ruff-blue)
77
![mypy](https://img.shields.io/badge/type--check-mypy%20strict-blue)
@@ -29,7 +29,7 @@ A comprehensive Windows registry tweak toolkit with **1 292 tweaks** across **69
2929
- **Corporate network safety** — blocks tweaks on domain-joined, Azure AD, VPN, and managed machines
3030
- **Automatic backups** — every registry mutation is backed up before changes with rollback on error
3131
- **Export PowerShell** — generate `.ps1` scripts from selected tweaks for portable deployment
32-
- **~17 378 tests** across 21 test files — full smoke, CLI, GUI, and engine coverage
32+
- **~17 511 tests** across 21 test files — full smoke, CLI, GUI, and engine coverage
3333

3434
## Architecture
3535

docs/API.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ class TweakResult(str, Enum):
7171
| `remove_tweaks(ids, ...)` | `→ dict[str, TweakResult]` | Batch remove by ID list |
7272
| `tweaks_by_ids(ids)` | `→ list[TweakDef]` | Resolve IDs to TweakDef objects (unknown IDs silently skipped) |
7373
| `tweaks_by_tag(tag)` | `→ list[TweakDef]` | All tweaks carrying the given tag (case-insensitive) |
74+
| `tweaks_by_scope(scope)` | `→ list[TweakDef]` | All tweaks matching `"user"`, `"machine"`, or `"both"` |
75+
| `tweaks_above_build(build)` | `→ list[TweakDef]` | Tweaks with `min_build <= build` |
76+
| `tweak_risk_level(td)` | `→ str` | `"low"`, `"medium"`, or `"high"` risk classification |
77+
| `tweak_count_by_scope()` | `→ dict[str, int]` | Counts per scope key (`user/machine/both`) |
78+
| `category_counts()` | `→ dict[str, int]` | Tweak count per category name |
7479

7580
---
7681

@@ -87,11 +92,15 @@ SESSION.set_dword(path, name, value) # REG_DWORD
8792
SESSION.set_string(path, name, value) # REG_SZ
8893
SESSION.set_binary(path, name, data) # REG_BINARY
8994
SESSION.set_qword(path, name, value) # REG_QWORD (64-bit int)
95+
SESSION.set_expand_string(path, name, v) # REG_EXPAND_SZ
96+
SESSION.set_multi_sz(path, name, values) # REG_MULTI_SZ (list[str])
9097
SESSION.set_value(path, name, val, type) # any type
9198
SESSION.read_dword(path, name) → int|None
9299
SESSION.read_string(path, name) → str|None
93100
SESSION.read_binary(path, name) → bytes|None
94101
SESSION.read_qword(path, name) → int|None
102+
SESSION.read_expand_string(path, name) → str|None
103+
SESSION.read_multi_sz(path, name) → list[str]|None
95104
SESSION.list_values(path) → list[tuple[str, object, int]]
96105
SESSION.list_keys(path) → list[str]
97106
SESSION.key_exists(path) → bool
@@ -136,6 +145,11 @@ python -m regilattice --diff gaming # delta vs profile
136145
python -m regilattice --dry-run --list # dry-run mode
137146
python -m regilattice --search "telemetry" # search tweaks
138147
python -m regilattice --search "telemetry" --output json # search as JSON
148+
python -m regilattice --list --scope user # filter by registry scope
149+
python -m regilattice --list --scope machine # machine-scope tweaks only
150+
python -m regilattice --list --min-build 22621 # Win 11 22H2+ tweaks only
151+
python -m regilattice --list --corp-safe # HKCU-only tweaks
152+
python -m regilattice --list --needs-admin # admin-required tweaks
139153
python -m regilattice --export-json out.json # export as JSON
140154
python -m regilattice --import-json in.json # import selection
141155
python -m regilattice --export-reg out.reg # export registry
@@ -153,6 +167,8 @@ config.force_corp # bool — bypass corporate check
153167
config.max_workers # int — thread pool size
154168
config.backup_dir # Path — backup directory
155169
config.auto_backup # bool — automatic backups
170+
config.theme # str — UI theme ("system" | "mocha" | "latte" | "nord" | "dracula")
171+
config.locale # str — UI language tag (default "en")
156172
```
157173

158174
---
@@ -165,8 +181,8 @@ Corporate network detection.
165181
|----------|-------------|
166182
| `is_corporate_network()` | `→ bool` — True if corp environment detected |
167183
| `assert_not_corporate(force=False)` | Raises `CorporateNetworkError` if corp |
168-
| `corp_guard_status()` | `→ dict` — Detailed detection results |
169-
184+
| `corp_guard_status()` | `→ dict` — Detailed detection results || `corp_guard_reasons()` | `→ list[str]` — Copy of reasons list from last detection |
185+
| `reset_corp_cache()` | Clear cached detection result (useful in tests/hot-reload) |
170186
---
171187

172188
### `regilattice.elevation`
@@ -190,15 +206,29 @@ Local-only usage analytics (no data sent anywhere).
190206
|----------|-------------|
191207
| `record_apply(tweak_id)` | Record a successful apply |
192208
| `record_remove(tweak_id)` | Record a successful remove |
193-
| `record_error()` | Record an error |
209+
| `record_error()` | Record a generic error |
210+
| `record_error_for(tweak_id)` | Record a per-tweak error (increments both global and per-ID counters) |
194211
| `record_session()` | Record session start |
195212
| `get_stats()` | `→ AnalyticsData` |
213+
| `error_stats()` | `→ dict[str, int]` — per-tweak error counts |
196214
| `top_tweaks(n=10)` | `→ list[tuple[str, int]]` — most applied |
197215
| `reset()` | Clear all analytics |
198216

199-
---
217+
### `regilattice.ratings`
218+
219+
Local tweak rating system (1–5 stars + optional notes).
200220

201-
## GUI Modules
221+
| Function | Description |
222+
|----------|-----------|
223+
| `rate_tweak(tweak_id, stars, note="")` | Set rating for a tweak (1–5 stars) |
224+
| `get_rating(tweak_id)` | `→ RatingEntry \| None` — retrieve stored rating |
225+
| `all_ratings()` | `→ dict[str, RatingEntry]` — all rated tweaks |
226+
| `remove_rating(tweak_id)` | Delete rating for a tweak |
227+
| `top_rated(n=10)` | `→ list[tuple[str, int]]` — highest-rated tweaks |
228+
| `average_rating()` | `→ float \| None` — mean stars across all rated tweaks |
229+
| `rated_count()` | `→ int` — number of tweaks that have been rated |
230+
231+
---
202232

203233
| Module | Key Exports |
204234
|--------|-------------|

tests/test_cli.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -853,13 +853,12 @@ def test_scope_both_exits_zero(self, capsys: pytest.CaptureFixture[str]) -> None
853853

854854
def test_scope_invalid_rejected(self) -> None:
855855
"""argparse should reject invalid --scope values."""
856-
import sys
857856
with pytest.raises(SystemExit) as exc_info:
858857
_build_parser().parse_args(["--list", "--scope", "invalid"])
859858
assert exc_info.value.code != 0
860859

861860
def test_scope_user_json_only_user_tweaks(self, capsys: pytest.CaptureFixture[str]) -> None:
862-
from regilattice.tweaks import tweak_scope, all_tweaks
861+
from regilattice.tweaks import all_tweaks, tweak_scope
863862

864863
rc = main(["--list", "--scope", "user", "--output", "json"])
865864
assert rc == 0
@@ -869,7 +868,7 @@ def test_scope_user_json_only_user_tweaks(self, capsys: pytest.CaptureFixture[st
869868
assert item["id"] in all_user_ids
870869

871870
def test_scope_machine_json_only_machine_tweaks(self, capsys: pytest.CaptureFixture[str]) -> None:
872-
from regilattice.tweaks import tweak_scope, all_tweaks
871+
from regilattice.tweaks import all_tweaks, tweak_scope
873872

874873
rc = main(["--list", "--scope", "machine", "--output", "json"])
875874
assert rc == 0

tests/test_registry.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,6 @@ def test_read_multi_sz_returns_list_on_match(self, tmp_path: Path) -> None:
632632

633633
@pytest.mark.skipif(not is_windows(), reason="registry requires Windows")
634634
def test_read_multi_sz_returns_none_on_type_mismatch(self, tmp_path: Path) -> None:
635-
import winreg
636635

637636
session = RegistrySession(base_dir=tmp_path)
638637
fake_key = MagicMock()
@@ -680,7 +679,6 @@ class TestCacheInvalidationExtended:
680679

681680
@pytest.mark.skipif(not is_windows(), reason="registry requires Windows")
682681
def test_invalidate_clears_binary_cache(self, tmp_path: Path) -> None:
683-
import winreg
684682

685683
session = RegistrySession(base_dir=tmp_path)
686684
path = r"HKEY_CURRENT_USER\Software\InvalTest"
@@ -700,7 +698,6 @@ def test_invalidate_clears_binary_cache(self, tmp_path: Path) -> None:
700698

701699
@pytest.mark.skipif(not is_windows(), reason="registry requires Windows")
702700
def test_invalidate_clears_qword_cache(self, tmp_path: Path) -> None:
703-
import winreg
704701

705702
session = RegistrySession(base_dir=tmp_path)
706703
path = r"HKEY_CURRENT_USER\Software\InvalTest"

tests/test_tweaks_init.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@
3636
save_snapshot,
3737
search_tweaks,
3838
status_map,
39+
tweak_count_by_scope,
3940
tweak_risk_level,
4041
tweak_scope,
4142
tweak_status,
42-
tweak_count_by_scope,
4343
tweaks_above_build,
4444
tweaks_by_category,
4545
tweaks_by_scope,
@@ -1532,7 +1532,10 @@ def test_parallel_detect_free_gets_unknown(self) -> None:
15321532

15331533
def test_progress_fn_called_for_detect_free_parallel(self) -> None:
15341534
calls: list[tuple[int, int]] = []
1535-
no_detect = TweakDef(id="__sm_prog_nd__", label="T", category="C", apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=None)
1535+
no_detect = TweakDef(
1536+
id="__sm_prog_nd__", label="T", category="C",
1537+
apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=None,
1538+
)
15361539
with (
15371540
patch("regilattice.tweaks._TWEAK_INDEX", {"__sm_prog_nd__": no_detect}),
15381541
patch("regilattice.tweaks._ALL_TWEAKS", [no_detect]),
@@ -1543,7 +1546,10 @@ def test_progress_fn_called_for_detect_free_parallel(self) -> None:
15431546

15441547
def test_detect_fn_present_executed_sequentially(self) -> None:
15451548
detect_fn = MagicMock(return_value=True)
1546-
with_detect = TweakDef(id="__sm_seq_det__", label="T", category="C", apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=detect_fn)
1549+
with_detect = TweakDef(
1550+
id="__sm_seq_det__", label="T", category="C",
1551+
apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=detect_fn,
1552+
)
15471553
with (
15481554
patch("regilattice.tweaks._TWEAK_INDEX", {"__sm_seq_det__": with_detect}),
15491555
patch("regilattice.tweaks._ALL_TWEAKS", [with_detect]),
@@ -1554,8 +1560,14 @@ def test_detect_fn_present_executed_sequentially(self) -> None:
15541560

15551561
def test_mixed_sequential_detect_and_no_detect(self) -> None:
15561562
detect_fn = MagicMock(return_value=False)
1557-
td_with = TweakDef(id="__sm_mix_det__", label="W", category="C", apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=detect_fn)
1558-
td_without = TweakDef(id="__sm_mix_nd__", label="X", category="C", apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=None)
1563+
td_with = TweakDef(
1564+
id="__sm_mix_det__", label="W", category="C",
1565+
apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=detect_fn,
1566+
)
1567+
td_without = TweakDef(
1568+
id="__sm_mix_nd__", label="X", category="C",
1569+
apply_fn=MagicMock(), remove_fn=MagicMock(), corp_safe=True, detect_fn=None,
1570+
)
15591571
index = {"__sm_mix_det__": td_with, "__sm_mix_nd__": td_without}
15601572
with (
15611573
patch("regilattice.tweaks._TWEAK_INDEX", index),

0 commit comments

Comments
 (0)