Skip to content

feat: redesign full tweak system - #32

Open
ehsan18t wants to merge 52 commits into
mainfrom
tweak-system-redesign
Open

feat: redesign full tweak system#32
ehsan18t wants to merge 52 commits into
mainfrom
tweak-system-redesign

Conversation

@ehsan18t

Copy link
Copy Markdown
Owner

No description provided.

ehsan18t added 11 commits July 21, 2026 07:12
First-principles design for the effect/apply/snapshot layer: one typed Effect model, per-tweak elevation floor with per-step escalation and TI acquire-vs-operate handling, ephemeral actions, hosts/firewall kept as typed kinds, build-number OS scoping, and a snapshot-history / Restore-Snapshot model.

Rewrite ADR-0003 (System Default is a computed status, not a revert target), amend ADR-0002 (snapshot history + checkable-only compare), add ADR-0005 (elevation is declared per tweak, never silently escalated). Remove the superseded TWEAK_SYSTEM_REVIEW.md.
Expand the §6 example to show an HKCU (floor carve-out) effect and a typed Hosts setting, and fix the §4 diagram (restore, hosts/firewall kinds).

Reconcile snapshot/restore wording across the spec after the redesign: history of return-points (not a single baseline), Restore Snapshot vs revert, per-option detectability, and System Default as a status rather than a selectable target. Rename the ADR-0003 file to match its rewritten 'computed status' conclusion.
…lementation plan

Fold the full design-review and grilling outcomes into the binding docs:
presence semantics, shared claim/release ownership, restore-as-reapply,
.reg-aligned authoring surface, merged registry paths, the redesigned
Windows-version model, Unknown/Unavailable statuses, and the WAL action
journal.

- spec rev 2 with 26 normative invariants and a resolved-decision log
- ADR-0002/0003/0004/0005 amended; ADR-0006 (one address, one owner) and
  ADR-0007 (option snapshots are references) added
- CONTEXT.md rebuilt on the rev-2 vocabulary
- implementation plan for the engine rebuild and hard cutover
@ehsan18t
ehsan18t force-pushed the tweak-system-redesign branch 2 times, most recently from 1723ae1 to d19efc0 Compare July 22, 2026 17:39
ehsan18t added 10 commits July 23, 2026 11:43
Implements ActionKind (run_apply/run_undo/run_probe) for the imperative
Action escape hatch: cmd/powershell scripts via -EncodedCommand (Cmd via
a temp script file, since cmd.exe has no encoded-command flag) plus
DeleteTree reusing the hardened registry key-delete through
RegistryKind::drive. Exit 0 is success/present; non-zero is a typed
ActionFailed(code); a script that fails to spawn or exceeds the 30s
hand-rolled timeout is ActionExecFailed, never a benign Ok. No-undo and
no-probe calls return Error::Invalid rather than a silently misleading
success/false. Adds the missing build guard rejecting an ephemeral
action that also declares undo/probe, with a fixture pinning it.
Implements the apply pipeline and ADR-0001 rollback (spec §8.1): capture
pre-apply state before any mutation, persist the WAL journal before
driving a single effect, drive every effect in declaration order with
did-it-work verification, and on any failure undo completed actions in
reverse plus drive the captured state back -- a verified full rollback
consumes the snapshot entry, an incomplete one keeps it as Needs
Attention. Adds the per-tweak async lock and crash-residue scan
(lifecycle.rs), and a new ActionRunner seam (engine/mod.rs) so Action
apply/undo can be mocked in tests exactly like the existing ProbeSource
seam, keeping the whole suite at zero OS contact.
…ller, winver runtime

- winver.rs: real running Windows version via RtlGetVersion + UBR; WindowsScope::applies
  implements the full products/build/revision grammar for runtime scope decisions, while
  validate.rs's Milestone-based build-time guards stay untouched.
- engine/context.rs: effective_level (max, escalate-only), route (HKCU always in-process as
  the interactive user), group_steps (consecutive System/TI batched, User/Admin never
  grouped), and the over-the-shoulder SID guard behind an injectable probe.
- broker.rs: run_ops adds a multi-op caller beside run_one/run_elevated_broker (unchanged
  wire protocol), distinguishing couldn't-acquire-the-child from acquired-but-op-failed.
- engine/mod.rs (AllKinds::drive): routes System/TI drives through the broker via a
  mechanical Setting -> BrokerOp translation per kind (registry/service/task); Hosts/
  Firewall and field-addressed registry writes remain unrouted (UnsupportedLevel). Engine-
  level grouping of consecutive effects is not yet wired -- group_steps is a primitive for
  a follow-on task.
- kinds/mod.rs: new CouldNotAcquireElevation error variant, distinct from the existing
  AccessDenied (now reused for "acquired but denied").

Manually verified one grouped System-level batch (RegSet -> RegDeleteValue -> RegDeleteKey)
end-to-end against the built magicx-toolbox.exe --broker mode while elevated; confirmed via
an external PowerShell Test-Path that no residue remained afterward. See
.superpowers/sdd/task-14-report.md for the full transcript and the cargo-test-harness
limitation that prevented shipping this as a passing #[ignore]d test.
…ine and corpus

Author examples.yaml (8 tweaks: registry tri-state absent, service+task with
optional/if_missing, hosts+firewall, action with probe+undo+ephemeral, a
shared claim/release pair, a packed-value field, and a windows-scoped
tweak), rewire build.rs to load+validate it via the new model/parse/
validate/schema pipeline and embed the compiled corpus, and prove the
engine end-to-end against real HKCU state in tweaks::e2e_tests.

E2E testing surfaced a real gap in restore's Values-dump path: it never
released a Shared claim the reverting tweak held (only the OptionRef path
drove Shared effects), permanently stranding a shared value away from its
captured original on the common "first apply from a virgin machine claims
a shared setting" path. Fixed in engine::revert.

Hard cut, no dual pipeline: deleted the old option-centric schema
(models/tweak_schema.rs, models/tweak.rs, models/inspection.rs,
models/tweak_snapshot.rs), services/backup/, the old tweak/backup Tauri
commands, services/tweak_loader.rs, and all 9 old tweak YAML files. The
low-level Windows types those primitives still need (RegistryHive,
RegistryValueType, ServiceStartupType, SchedulerAction, HostsChange,
FirewallChange, and siblings) were extracted verbatim to
models/win_types.rs first. Orphaned elevation/service helpers whose only
callers were the deleted pipeline were removed to keep the gate clean.

The tweak Tauri command surface is intentionally empty until Task 16.
@ehsan18t
ehsan18t force-pushed the tweak-system-redesign branch from d19efc0 to b7b6ef8 Compare July 23, 2026 05:48
ehsan18t added 6 commits July 23, 2026 12:12
Adds the thin Tauri command layer over the redesigned tweak engine: get_tweaks,
get_statuses_stream (background-progressive scan emitting one tweak-status event
per tweak), apply_tweak, restore_tweak, list_snapshot_entries, discard_snapshot_entry,
rescan_after_elevation, and get_elevation_state. SnapshotStore/ClaimsStore/ProbeCache
are managed once via Tauri state and shared across every call; apply_tweak/restore_tweak
refuse (typed error) a tweak whose elevation floor or SID state disables it, before ever
reaching the engine. Wires the crash-residue scan at startup and re-prefixes a restore
failure's wording at the command boundary rather than in the engine.
Rewrite the API layer to the 8 Task-16 commands + the tweak-status event, and
adapt the tweak stores to fill statuses in incrementally as events arrive. Apply
is by option label; restore is a single head-walk; discard replaces keep-current-
state. Batch flows become client-side loops (no backend batch command). The card
and details modal render the new states (Unknown/Unavailable/Residue/held-by/
Needs-Attention, availability + SID-mismatch) with existing primitives; 1 option
renders a Switch, >=2 a Select. Categories are derived from the model. Profiles
stay disabled (already neutralized). No src-tauri changes.
Author-facing and end-user documentation uses commas, colons, parentheses,
semicolons, or separate sentences instead of em dashes.
ehsan18t added 7 commits July 27, 2026 10:49
The over-the-shoulder guard read the console session's user via
WTSQueryUserToken, which requires SE_TCB_NAME. Only LocalSystem holds
that privilege, so the call returned ERROR_PRIVILEGE_NOT_HELD on every
real run, elevated or not. The guard's fail-closed arm read that
guaranteed failure as a mismatch and refused all 54 elevation: user
tweaks. Measured at both privilege levels before and after.

Three faults, fixed together:

Probe. ProcessIdToSessionId -> WTSQuerySessionInformationW ->
LookupAccountNameW needs no privilege. It also asks about the process's
own session rather than the console session, since under RDP those
legitimately belong to different users and comparing against the console
reports a mismatch between accounts that were never in conflict. When
name resolution fails (domain-joined with an unreachable DC, or Entra),
it falls back to comparing SAM account names, which need no directory;
names are only ever compared with names, so a half-resolved state
degrades to "unknown" instead of accusing another account.

Keying. The guard consulted the tweak's elevation floor while routing
consulted the hive. The floor states which privilege a tweak needs, the
hive states whose state it changes, and 31 admin-floor tweaks in the
corpus drive HKCU effects that went unguarded while the user-floor ones
were blocked wholesale. Both now go through one corpus-aware
effect_is_hkcu, which also resolves shared blocks and DeleteTree
actions; a corpus-wide test asserts the two agree for every tweak.

Honesty. An unreadable SID still blocks, but reports a distinct
SidUnknown state instead of claiming another administrator elevated the
app. Fail-closed is deliberate: refusing mutates nothing, while a
wrong-account write has no return point, because the snapshot store is
keyed to the machine and apply verifies a write by reading back the same
hive it just wrote.

Also replaces is_running_as_admin's HKLM KEY_WRITE probe, which
collapsed every error kind through .is_ok(), with
GetTokenInformation(TokenElevation), and rejects HKCU addresses in
to_broker_op so the routing convention becomes a typed guarantee.

Reasoning and the rejected fail-open alternative: ADR-0005 amendment and
docs/plans/fix-hkcu-user-level-gate.md.
Replaces the demonstration corpus with eight real category files, built from an
independent four-pass validation of every tweak against primary sources.

203 tweaks in, 255 out: 10 deleted, 62 added, 166 corrected. A new `ai` category
collects the Recall, Click to Do, Copilot and in-box AI controls that were
scattered across privacy, interface and debloat.

Twelve tweaks did not work as authored. ssd_optimize_trim wrote a value name
Windows does not read; ultimate_performance_power_plan chased a GUID that
/duplicatescheme had already replaced; asr_block_lsass_theft carried a GUID one
character off Microsoft's, which Defender silently ignores;
enable_credential_guard wrote to the state mirror instead of the input key.

Three reverts were worse than never applying the tweak. Reverting
disable_online_speech_recognition fabricated a cloud-speech consent the user
never gave, and reverting require_smb_signing disabled a protection 24H2 enables
by default. Both now restore the real stock state.

Every tweak carries an info block in one shape: hook, what it does, benefits,
drawbacks, good to know, a recommendation that takes a position, and an evidence
block naming the risk level, whether the mechanism is Microsoft-documented or
community-corroborated, and its sources.

Names and option labels say what the control does. A tweak with one or two
options renders as a segmented switch with a tick, a cross and no text, so the
name is the only thing telling the user which way the tick goes: all 229 such
tweaks start with a verb naming what their first option does, and the 26 with
three or more keep noun names because their dropdown spells out every option.
No option is labelled the Windows default. What Microsoft ships today is a fact
about this month's Windows, not about the control, and pinning it into a label
means an edit every time Microsoft changes its mind. An option that writes
`absent` is named for where absence actually lands the machine, so deleting the
WDigest policy reads as Enabled rather than "not configured".

Three labels read backwards and are fixed. disable_lmhash_storage said
"Enabled", meaning the prevention was on, which beside its title read as "LM
hash storage: Enabled". filter_admin_token said "Enabled" while its subject is
the built-in Administrator account. remote_uac_token_filter offered "Not
filtered" writing `absent`, but absent behaves exactly as 0, so both options left
the token filtered and the label named a state it could not reach.

Four addresses were claimed by two tweaks each, visible only once all eight files
compiled together. In every case the dedicated tweak keeps the address and the
bundle drops it: disable_ui_animations over optimize_visual_effects,
disable_wmp_network_sharing over disable_ssdp_upnp, task_autochk_proxy over
disable_ceip_tasks.

Research and sources: docs/superpowers/research/validation/
Every source, correction and verdict behind the corpus rebuild, one document per
category plus the cross-cutting analyses.

Four passes: a first validation round, an adversarial round that re-attacked the
87 entries the first round had called clean and flipped 27 of them, a gap hunt
that proposed 71 controls the corpus lacked, and a rewrite of every tweak's
user-facing copy into one template.

The evidence rule is in _SPEC.md and is deliberately not Microsoft-only.
Microsoft documents what it wants administrators to configure, not everything the
OS reads, so treating "no Microsoft page" as "doubtful" penalises working tweaks.
Three or more genuinely independent community sources agreeing on key, value
name, type and semantics also verify, labelled so the reader knows which kind of
confidence they are getting. Under that rule all 14 UNVERIFIED verdicts from the
first pass resolved.

_SPEC.md also carries the rule the option labels follow: an option is a state the
app drives the machine to, so every literal in one has to be established, and no
option is ever labelled the Windows or stock default.

_harmful-revert.md carries the class that outranks the rest: a wrong mechanism
makes a tweak inert and the user investigates, while a wrong revert does damage
at the moment the user is trying to be careful.

A clean 24H2 image baseline is the one missing input, tracked in the README's
open questions.
…weak info

Three things the new corpus needs from the frontend.

Categories now come from the corpus. Registers the get_categories command and
its binding, so the sidebar reads id, name, icon and description from the
compiled corpus instead of a hardcoded list, and adding a category file is the
only step needed to make it appear. The icons the eight files name are
registered, including robot-outline for the new AI category. Corpus category
names are longer than the old hardcoded labels, so the sidebar label truncates
rather than forcing the row wider, and the count badge no longer shrinks for it.

Info blocks now end with an evidence section citing two to four sources as
markdown links, which the renderer emitted as literal bracket-and-paren text.
Links are extracted before the emphasis passes and shielded behind a sentinel so
an underscore inside a URL can never become an <em>. Only http and https become
anchors; any other scheme renders as plain text, because an href the webview
navigates natively is a script-execution path and this is a shared component
that may one day render something less trusted than the corpus. The body is
rendered with {@html}, so no component can be embedded per anchor; one delegated
click handler routes anchor clicks through the Tauri opener, matching
ExternalLink.

Also rewrites the profile export note, which ran two clauses together across an
em dash that the project style forbids.
… when it is the state

The card treated two authored options as a toggle and picked the OFF side by
matching "(Stock Default)" in a label. With that convention gone the regex
matched nothing, and a dropdown for a two-way choice was the wrong control
anyway. SegmentedSwitch already existed for this and had no callers; it now
renders every tweak with one or two authored options, and only three or more
fall through to the dropdown.

System Default was also offered on every tweak as a permanent extra position. It
is a computed status, not a state anyone picks: it means the live surface matched
none of the authored options, so it only exists while that is true. It now
appears, selected, exactly when nothing matches and disappears once an option
does. The way back out of an applied option is Restore, which is its own button.

SegmentedSwitch gains per-segment disabled so an option the machine cannot
satisfy is shown and dimmed rather than hidden, with arrow-key navigation
skipping it.
System Default was the one status that told the user nothing. Active names an
option they can go read and Unknown lists the effects it could not read, but
System Default said only that the machine matches none of the options, with no
way to see what it does match.

detect already reads the live value of every applicable effect into a map to do
the matching, then discarded it. That map is now returned as `observed`, and the
command layer pushes each reading through `push_setting_change`, the very same
builder the authored options use. The result is an option-shaped block labelled
"Your system right now", rendered by a snippet extracted from the option markup,
so the system state and the options it failed to match are drawn by identical
components with identical addresses and formatting. The user compares like with
like instead of decoding a value dump.

Beneath it sits the agreement list: which option each individual setting agrees
with. A surface split across options is the usual reason nothing matched, and
this names exactly where the machine straddles.

Populated for SystemDefault only, so the per-tweak status event for the rest of
the corpus is unchanged and the panel's whole render guard is its presence.
Building it needs the tweak, which `From<TweakStatus>` does not have, so the
conversion leaves it `None` and `scan_and_emit` fills it in. The status view and
its two embedders drop `Eq` because the change views carry `serde_json::Value`,
which is `PartialEq` but not `Eq`. The restore path leaves it empty on purpose;
the next detect fills it in.
Four things the corpus rebuild made stale or wrong.

CONTEXT.md told authors to write option labels like "500 KB (Stock Default)",
which is where the corpus convention came from. Stock Default stays as a term
for research and info prose, where explaining what Windows does is the point,
and is now barred from option labels: it encodes what Microsoft ships this month
into data that outlives the decision. The Option entry states the rule the labels
follow, that an option is a state we drive the machine to, so every literal in
one must be sourced and where a state cannot be established the tweak authors
fewer options rather than inventing one.

ARCHITECTURE.md, TWEAK_AUTHORING.md and TWEAK_SYSTEM.md still described the
redesign's demonstration corpus as what ships, with ten references to an
examples.yaml that no longer exists. The worked examples in the authoring guide
stay as they are, since they were always self-contained illustrations checked
against the validator rather than excerpts; the guide now says so and notes that
their MagicXToolboxExample addresses are deliberate placeholders.

KNOWN_ISSUES.md records the defects we have accepted for now, as bugs to fix
rather than settled behaviour.

PRE_MERGE_TASKS.md tracks what must land before this branch merges, including
the shared-address multi-value analysis kept as reference so the deferral does
not have to be re-derived. The file removes itself as part of the merge.
@ehsan18t
ehsan18t force-pushed the tweak-system-redesign branch from 748527f to 8eb7bc6 Compare July 27, 2026 04:57
ehsan18t added 18 commits July 27, 2026 22:33
The backend detect scan began emitting tweak-status events before the
frontend had built its tweak list, so the earliest events were dropped
silently by setStatusView and loadModel then reset every status to a
fresh loading placeholder. Nothing re-sent them, leaving the first few
tweaks in corpus order stuck on Checking for the life of the session.

Await initializeQuick() before starting the stream (Svelte mounts +page
before +layout, so loadRemainingData kicked the scan first), and buffer
any status view that still arrives ahead of its tweak so loadModel seeds
from it instead of overwriting it.
The per-effect agreement rows under "Your system right now" printed the
author's YAML effect id (box_suggestions, bing_enabled), an internal
handle that lets an option set a value without repeating its address.
It means nothing to someone reading the change list directly above it.

Carry the concrete address on the view instead: the registry value name,
service name, task name, hosts domain or firewall rule name, matching how
the change list already names each one. Paths are dropped, since the row
above carries the full address and the short name is what tells two rows
apart. The effect id stays on the view as the render key, which display
names cannot serve because they are not unique within a tweak.
…y SYSTEM path

`SvcStart`, `SvcStop`, `Powershell`, and `RawCmd` had no producer: no
`to_broker_op`/`to_broker_ops` translation ever emitted them. They stayed live in
the request deserializer, so anything able to hand the elevated child a request
file could reach two interpreters (`powershell -EncodedCommand` and `cmd /c`)
running as SYSTEM or TrustedInstaller. Removing them also removes the broker's
duplicate base64 encoder, which action.rs already flagged as a carry-forward.

Dropping the two service ops left `service_control::start_service`/`stop_service`
and their poll/dependent-enumeration helpers with no callers. The tweak schema
drives a service to a startup type and never starts or stops one, so those go
too; git keeps them if a future effect kind needs them back.

The single-op path went the same way. `write_registry_json_value`'s `use_system`
branch was unreachable (its only caller, the broker, hardcodes false), which made
`set_registry_value_as_system`, `run_one`, and `into_single` dead. `run_ops` is
now the module's only entry point. Also drops the `trusted_installer` alias that
made call sites name a module that no longer exists.
…ing as success

`run_ops` scanned `results: Vec<OpOutcome>` for an `Err` and returned `Ok(())` if it
found none. It never checked the length, so a response carrying fewer results than
ops were sent, with none of them an error, reported the whole batch as applied. The
executor never produces that shape, but nothing in the protocol said it could not,
and the engine's read-back verify does not cover it: driving a service to Disabled
sends `SvcSetStartup` plus a `DelayedAutostart` companion write, and `read_service`
only inspects that flag when the primary type is Automatic. A dropped companion
write would verify clean and leave a stale `DelayedAutostart=1` behind.

The response now says what happened rather than listing what was attempted:
`attempted: usize` plus `failure: Option<OpFailure>`. Success requires both no named
failure and a full attempt count, so the shape that used to pass silently is now
unrepresentable as a success. `OpOutcome` and the positional-results contract are
gone with it.

The decision moves into `check_response`, isolated from the spawn so it is testable
against responses the executor would never emit, matching how `map_status` and
`map_task_state` already separate a decision from its OS call.
…hat clears it

`CloseHandle` overwrites the calling thread's last-error, so six failure paths were
reporting the close's result rather than the call that actually failed:
`LookupPrivilegeValue`, `AdjustTokenPrivileges`, `OpenProcessToken`,
`DuplicateTokenEx`, `CreateProcessWithTokenW`, and `CreateProcessW`. Diagnostics
only, never a false success, but "OpenProcess failed: 0" sends anyone debugging an
elevation failure in the wrong direction. `wait_and_reap` already got this right;
the rule is now stated in the module docs and enforced by a `close_then` helper that
takes the already-built error, so the last-error cannot be read after the close.

Alongside that, three simplifications in the same code: `find_process_by_name`'s
hand-rolled UTF-16 compare loop and its `char_to_lower` become
`from_utf16_lossy` + `eq_ignore_ascii_case`; the two `STARTUPINFOW` literals that
spelled out fifteen null fields each become a shared `hidden_startup_info`; and
`common.rs` no longer re-exports forty `windows_sys` symbols, so each spawn file
now names the Win32 surface it actually uses.

The TI path sets `cb` to `size_of::<STARTUPINFOEXW>()` explicitly after taking the
shared startup info, since `EXTENDED_STARTUPINFO_PRESENT` requires the size to span
the extended struct or the parent-process attribute list is ignored.
…uest file

The broker wrote its op list to a predictable `%TEMP%` path with `fs::write`, closed
the handle, then spawned a SYSTEM or TrustedInstaller child to read it back. Every
process running as this user can write that directory, including Medium-integrity
ones that never passed a UAC prompt, and the window is not tight: acquiring the TI
token alone polls the service for up to ten seconds before the child even starts.
Winning that race meant choosing what ran as SYSTEM.

action.rs already had the fix for this exact threat, on the less privileged path.
That guard moves into `services::exclusive_temp` and both paths now share it: a
CSPRNG-derived filename, `create_new` so a pre-planted path fails loudly, and a
write handle held open with only FILE_SHARE_READ so the intended reader can read
while nothing else can rewrite, delete, or rename. Its tests pin the OS behaviour
being relied on, including the ERROR_SHARING_VIOLATION a second writer now gets.

The response file is a weaker case and the docs now say so rather than implying it
is closed. The child creates it, so the parent cannot hold it open the same way; it
gets an unpredictable path, and the exit-code gate and nonce still stand, but a
same-user process that wins the post-exit race and guesses the batch length could
forge a success. Handing the child an inherited pipe instead of a path is what would
close it.

Transport-failure exit codes are named constants now, and the parent maps each back
to a phrase. Previously the child described them with `eprintln!` into a process
spawned with no console and no inherited stderr, so the text went nowhere and the
user saw a bare number.
…rth keeping

The comments across the elevation subsystem and its call sites had drifted into
recording their own development history: which task added a function, what it was
called before, which file stayed untouched so an earlier review would still hold,
which controller decision a test came from. Git records all of that, and a reader
looking at the code today has to filter through it to reach the part that explains
why the code is shaped this way.

Cut the archaeology, keep and sharpen the load-bearing rationale: why an HKCU
address must never become a broker op, why a batch stops at its first failure, why
`wait_and_reap` keeps three wait outcomes distinct, why the `DelayedAutostart`
companion write is unconditional, and why translation lives beside each address
shape while dispatch lives above every kind.

Three comments were also stale enough to mislead. `service.rs` and `task.rs` each
had a test asserting "this build cannot yet route System/Ti through the broker",
which stopped being true when that routing shipped: what those tests actually pin
is that the in-process kind refuses to escalate on its own when called directly,
bypassing the routing. Renamed and reworded to say that. `AccessDenied` now
mentions that it also carries "the elevated child ran but the op failed", which is
what a broker op failure maps to.
Section 7 still named the `trusted_installer` alias (removed) and listed
`run_powershell()`, `run_powershell_as_system()`, and `run_schtasks_as_system()`,
none of which exist. Replaced with the real surface: one `run_ops` entry point,
typed effects only, and how the two tokens are acquired.
Issue 2 was fixed by b3ccbfd, which removed `handleSwitchChange` and made System
Default its own segment routed through `goSystemDefault`, so it unstages first.
The entry described a handler that no longer exists. Verified the cancel path is
reachable from all three control shapes: one option, two options, and the 3+
dropdown.

The diagnosis itself is still literally true and now lives beside the code it
describes. `stageApply`'s cancel check compares against `activeOption`, which is
null at System Default by design, so it can never fire there. Any new control that
calls `stageApply` for that position would reintroduce the dead end, which is worth
knowing at the call site rather than in a bug list the entry has left.
Cancelling a staged change worked by clicking whatever is already live, which
`stageApply` implemented as `label === activeOption`. That comparison cannot match
at System Default, where `activeOption` is null by design (ADR-0003: no option is
active), so the System Default position had to be diverted to `goSystemDefault`
before it ever reached staging. Both call sites did that correctly, so nothing was
broken, but the rule lived in each caller rather than in the code it protects: a
third control wired straight to `stageApply` would stage a change with no way back
to zero pending, which is exactly the bug KNOWN_ISSUES issue 2 described.

`stageApply` and `goSystemDefault` merge into one `selectTarget`, which takes the
clicked target and decides. System Default is checked first, so it cannot fall
through to staging no matter who calls. The two handlers now just forward, and the
duplicated System Default branch in each of them is gone.

No behaviour change: same three outcomes (restore, cancel, stage) for the same
inputs, now reachable by one path instead of two.
Measures where the status scan actually spends its time, by wrapping the existing
`EffectKind`/`ProbeSource` injection seams in a decorator that records call count
and elapsed time per work category. The production path runs unmodified; nothing
in detect.rs is mocked or touched.

Ignored by default since it reads the live registry, SCM, and Task Scheduler COM.

  cargo test --release scan_sweep_timing -- --ignored --nocapture
Measured with scan_bench: 12.25s serial, 2.02s parallel, 6.1x on 16 threads.

Detection is a pure read and tweaks are independent, but the sweep ran them one at
a time on a single thread. That is fine for the registry (375 reads total 6ms), and
ruinous for the 26 Action probes, which each spawn a PowerShell process and average
470ms. Serially those 26 are 12.22s of the 12.25s sweep.

Results stream back over a channel and are emitted on the calling thread, so `emit`
needs no `Send` bound and the progressive arrival the UI is built around is intact.
Events now arrive in completion order rather than corpus order; each names its
`tweak_id` and the frontend keys on it, so order never carried meaning.

`statuses_emit_incrementally` asserted positions, which completion order makes
meaningless. It now compares the sorted set of reported ids against the corpus,
which is the stronger assertion anyway: indexing the first three events cannot catch
a duplicate or a dropped tweak, and a set comparison can.

Nothing needed locking. `ProbeCache` is already behind a mutex, the snapshot and
claims stores are immutable path holders that re-read per call, and
`scheduler_service` already serializes COM activation behind its own lock with a
once-per-thread MTA init.

rayon was already a declared dependency with no user in src/.
…rocess

Measured with scan_bench: serial 12.25s to 6.41s, parallel 2.02s to 1.19s.
Spawning probes drop from 26 to 14.

`probe:` was script-only, so every detection signal cost a process. That is 178ms
of floor before the script runs, and detection pays it once per probeable Action on
every sweep. Two whole classes did not need an interpreter at all.

Twelve probes asked "is this one package installed", each in its own PowerShell.
Measured, asking about one package costs the same as listing every package: 424ms
for `Get-AppxPackage -AllUsers -Name X` against 416ms for the bare enumeration,
because the cost is the module load plus the WinRT walk and both are per-process.
`probe: { appx_absent: [...] }` is now answered from one enumeration the whole sweep
shares, built on first ask and cached in `ProbeCache` so the same invalidation that
drops a stale probe reading drops it too.

One probe read a single DWORD, which the app reads natively in 16 microseconds.
`probe: { registry: { key, name, equals } }` covers it. Honest scope: this is the
only probe in the corpus that qualifies. The other `Get-ItemProperty` uses sit
inside subkey enumerations or alongside powercfg, and stay scripts. The form earns
its place by making the fast path available to future detection-only checks, not by
what it saves today.

`Probe::Script` stays the escape hatch and still covers the fourteen that genuinely
need an interpreter (powercfg, DISM, auditpol, CIM, Get-Process). `remove_copilot_app`
stays a script deliberately: it prefix-matches PackageName where the other eleven
match DisplayName exactly, and converting it would change detection semantics on a
machine none of us can test against.

Native forms resolve in `detect::probe_cached`, the only layer holding the shared
state they read; `ProbeSource` keeps its original job of running the script and
remains what a test mocks.

Verified against the live machine rather than assumed: all twelve package probes and
six genuinely-installed control packages agree between the old per-package queries
and the shared index, in both the present and absent directions, case-insensitively.
An enumeration that fails surfaces as Err, never as an empty set, since empty would
read as "already removed" for every asking tweak.
`probe:` now takes a map as well as a script body, so the three tables and the
"inline strings only" callout that all said "the script body" were understating what
the field accepts.

Section 12.5 gains a "pick the cheapest form that answers the question" subsection
covering `appx_absent` and `registry`, each with the measurement that motivates it,
and says plainly when a script is still the right answer. The note authors most need
is that asking about one package costs the same as listing every package, so
hand-writing a per-tweak `Get-AppxPackage` check pays full price for an answer the
shared enumeration already holds.
…es if_missing

`optional` only ever guarded capture and detect, never the drive. An optional
effect whose resource was absent returned ResourceMissing from the drive, which
aborted the apply and rolled the whole tweak back, even though detect had
already mapped that same absent resource to its `if_missing` value and reported
the option as available and satisfied.

Apply now agrees with detect: when an option's desired value equals the effect's
`if_missing`, there is nothing to drive and nothing to verify, so the effect
records as a no-op. The tolerance stays narrow; a desired value that differs
from `if_missing` still fails typed and rolls back.

This affected shipped tweaks, not only new ones. privacy.yaml and services.yaml
already declare optional task effects that would hard-fail on any machine where
one of those tasks is absent.
…view

The details modal renders one section per change kind straight off
TweakOptionView. If push_setting_change ever stopped projecting a kind, that
section would render empty and the tweak would look registry-only while apply
and detect kept working perfectly, so no existing test would catch it.

Walks the whole corpus and asserts every valued Setting effect, plus every
claimed shared effect, lands in exactly one of the six change lists. Also
asserts the corpus genuinely exercises registry, service and scheduled-task
projection, so the check cannot pass vacuously if a kind leaves the corpus.
…ock tweaks

Moves the 13 Windows Update tweaks out of network.yaml into their own
windows_update category; what remains is renamed "Network & Power".

windows_update_mode replaces the narrower disable_auto_update_download with a
four-mode AU policy switch: automatic, download only, notify only, off by
policy. It owns the same two addresses, so the two cannot coexist.

block_update_pipeline stops the pipeline at its roots. Disabling wuauserv alone
does not hold, because the Windows Update Medic Service repairs it, so the Medic
service and both of its tasks are driven first; the service registers no start
triggers, so once those tasks are disabled it has no way to run. The tweak also
covers the usoclient scan tasks, the MusNotification prompt, the update UI, and
the network path, while deliberately leaving BITS, Delivery Optimization and the
Store install tasks alone.

Every scheduled-task effect runs at TrustedInstaller. On stock 24H2 the
UpdateOrchestrator task files grant Administrators read and execute only, and
the WaaSMedic pair carries a protected DACL, so an elevated administrator alone
cannot disable them; a too-low level would be a hard failure plus full rollback.
…emantics

- "eight category files" is now nine, in ARCHITECTURE, TWEAK_SYSTEM and
  TWEAK_AUTHORING
- CLAUDE.md pointed at models/tweak_schema.rs, which does not exist on this
  branch; the live schema is src/tweaks/{model,parse,schema,validate}.rs
- CLAUDE.md claimed the elevation broker runs PowerShell via -EncodedCommand.
  BrokerOp carries no script variant; PowerShell runs only through the action
  effect kind
- CLAUDE.md still described skip_validation and the "2 options to toggle" rule,
  neither of which exists in the current schema
- TWEAK_AUTHORING section 8.4 said only that a vanished resource fails typed,
  which did not cover the desired-equals-if_missing case now applied as a no-op
@ehsan18t ehsan18t changed the title Tweak system redesign feat: redesign full tweak system Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant