You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Eight new preferences, and a way to find any of them. Settings gained a Diff & code category and five appearance controls, which took the page past thirty switches — the point where grouping stops being enough, because knowing that word wrap lives under "Diff & code" means already knowing GitPulse's taxonomy. A filter box now narrows the category rail and the rows inside each panel, matching synonyms the labels never say ("side by side" finds the split-diff default). Every control is stamped with the catalog id it is registered under, and the catalog and the markup are compared in both directions: a control with no entry is unfindable by search, an entry with no control is a result that highlights nothing, and neither can ship.
An accent colour. Six choices tint selection, focus rings, the macOS glass and the commit graph's current-row marker. Each carries a separate light and dark shade — accent text sits on --c-surface-hover, which is near-black in one theme and near-white in the other, so one colour cannot be legible on both — and every pair is checked against WCAG AA by a test derived from the palette itself. --shadow-glow and --ring-focus are now expressed in terms of --c-accent rather than restated, so a teal window does not wear a blue focus ring; picking the default removes the override entirely and hands the theme back to the stylesheet.
Reduce motion, as an in-app preference. Deliberately one-way: leaving it off follows the system, and turning it on can only add reduction — a reader whose OS has asked for less motion cannot be overruled from a checkbox. It reaches both places motion is produced, the Svelte transitions and the CSS entrance animations, because either half alone is a setting that visibly half-works. The two triggers are mirrored rule for rule and a test pins the lists equal.
Timestamps as dates. Commit times can read 2026-09-06 instead of 3d ago — fixed width, sortable, and the same in every locale, which 06/09 is not. It reaches the commit list, branch tooltips, the diff's change picker, Work and the stack through one owner; whichever form is shown, hovering gives the other, and the commit list's column widens for a date rather than truncating one.
Diff defaults that survive the next file. Layout, word wrap, syntax highlighting and ignore-whitespace were fixed literals reset on every mount, so a reader who preferred split diffs re-selected split every time. They are now preferences that a diff opens at; the toolbar still owns the file in front of you. The whitespace default seeds each newly opened repository, and the copy names the wrap ceiling (4,000 rows) rather than offering a switch that silently does not apply above it.
Tab width — 2, 4 or 8 — everywhere code is drawn. Declared once on the document root, where tab-size inherits, so the diff, the file viewer, blame and the conflict editor all follow without each learning the preference exists. 8 is the CSS initial value and stays the default, so nothing renders differently until it is asked to.
Arrow keys in the settings category rail. It has always been a tablist of tabs and was one in name only: eight separate tab stops, none of which answered the arrow keys the role promises. It is now a single tab stop with Up/Down (wrapping), Home and End.
The macOS window is transparent, and the desktop behind it is blurred by the window server. GitPulse previously synthesised its own backdrop because native transparency was switched off; the blur you saw was the app blurring itself. macos-private-api plus a transparent window and an NSVisualEffectView (underWindowBackground) now put the real desktop behind the interface, blurred by macOS rather than by the application, which costs nothing per frame. Three settings have to be present together and any one alone is inert — the feature gate is deeper than its name, because tauri/macos-private-api forwards wry/transparent and wry compiles its setOpaque(false) call only behind that feature, so without it the WKWebView stays opaque and paints over a material that is working perfectly. This forfeits Mac App Store acceptance, which is the reason it had been disabled. macOS-only settings live in tauri.macos.conf.json, which Tauri merges by JSON Merge Patch — arrays are replaced, not merged, so that file restates the whole window entry and a test derives the expected object from the base config rather than trusting the copy.
A hue field, so a translucent surface has something to be translucent against. Four wide, saturated radial blobs span the shell and fade to their own colour at zero alpha rather than through the transparent keyword, which would dip the ramp through an unrelated hue on the way out. Sized to their own corners they left the middle of the window at the flat base colour — the one part of a window nobody can avoid looking at — so they are deliberately wide enough to overlap across the centre.
The MCP server now answers four capabilities, not one. It advertised tools and nothing else, so the only way to ask it anything was a tool call. It now implements every server method the 2026-07-28 schema defines: resources/list, resources/read and resources/templates/list expose the control plane as addressable gitpulse:// documents (eight repository facets as RFC 6570 templates, plus gitpulse://server/manifest and gitpulse://server/health); prompts/list and prompts/get ship four workflows that resolve against live state and embed it as resource blocks rather than telling the model to go and fetch it; and completion/complete completes repo_path for both. All four list operations are cursor-paginated. The method set is taken from the published schema, which is also why there is no ping and no logging/setLevel — neither exists in this revision, and inventing them would have been worse than omitting them.
gitpulse-hook, so the control plane can refuse an edit before it happens rather than describe it afterwards. Three hooks ship in plugins/gitpulse/hooks/hooks.json. collision-guard escalates a PreToolUse edit to the user when the same path is dirty in another worktree; command-gate routes the agent's Bash calls through the MANVI command gate the desktop app already uses, so git push --force is denied with the gate's own words instead of the gate applying to the GUI alone; session-brief injects a bounded repository brief at SessionStart. Each degrades to no decision plus a systemMessage saying the check did not run rather than to silent approval — a hook that cannot check must never look like one that checked and passed.
Fixed
hidden did not hide. The UA stylesheet's [hidden] { display: none } loses to any author rule that sets display, so a Tailwind flex utility on the same element silently outranked it — two settings rows kept rendering while marked hidden, which the filter depends on. [hidden] is now enforced at author level regardless of which utilities a row picks up later.
The theme segment could report a choice the app was no longer honouring. It snapshotted the preference at mount, and ⌘-shortcuts and the native View menu change it from outside the modal; reopening now re-reads it. A control that reads as selected while something else is in force is the same failure as a setting that does nothing.
The terminal stayed a solid slab, because two things under it paint black and neither is reachable from the stylesheet. Second instance of the graph canvas's shape, and the last surface in the app that owns its own paint. --bg-terminal resolving to transparent makes xterm paint nothing, so the surface is the plate its mount div already carries — a second translucent fill from the terminal would be exactly the double coverage the panes were just cured of. The token is deliberately not --bg-surface, which has a second runtime reader in the graph's node stroke; a stroke is not a fill. Three things are load-bearing there and none is cosmetic: allowTransparency must be set before open() and cannot be changed afterwards; the colour must be hex, because css.toColor in @xterm/xterm 6.0.0 parses #rgb/#rgba/#rrggbb/#rrggbbaa itself and pushes everything else through a canvas probe that throws when the sampled alpha is not 255 (measured in WebKit: rgba(20, 26, 41, 0.5) samples 128, the transparent keyword samples 0, and getComputedStyle returns exactly those forms); and xterm.css hard-codes background-color: #000 on its viewport, which is why a transparent theme colour on its own changed nothing visible.
The commit tooltip was the one float in the application with no blur at all, which is why it read as see-through rather than as glass: a filter that never runs looks exactly like one that does until content moves behind it. The float tier was written as .gp-card.shadow-float, which named the dialogs and missed everything else — the tooltip carries shadow-pop, and the toasts, the coach mark and the go-to-line popover carry shadow-float without .gp-card. The tier is now the float shadow, derived in a test from what the components actually wear, and those surfaces keep their denser bg-surface fill rather than the thinner glass one: a dialog card sits on a dimmed scrim, a tooltip sits straight on commit rows.
The sidebar's LOCAL, ORIGIN and TAGS rows wore a filled band that the folder rows beside them did not.bg-surface on a section header was a barely-visible tint against an opaque sidebar and a dark bar against a translucent one — the same coverage arithmetic as the recesses above. Nothing scrolls under those headers (the list is virtualised, not sticky), so the fill was decoration; it is gone, and the two kinds of group header now look alike.
The Work screen hugged the left edge and stopped a third of the way down the window. Nine bands each carried max-w-6xl with no centring, so a wide pane left a column of empty space on the right, and the empty states sat under the header with the rest of the pane blank beneath them. The scroller is a column now, every band is centred through one rule, and the empty states take the leftover height.
The commit-details preview was a black rectangle inside a glass panel. Same arithmetic as above, one rung down, where the author picked the alpha instead of the utility: bg-background/50 means "the base colour, thinned against the surface panel around me", which opaque is a nine-unit colour step and translucent is half a layer of coverage stacked on three others. Coverage is what darkens a glass stack regardless of the colour applying it — measured over a white desktop, the preview composited to rgb(22, 26, 36) beside a panel at rgb(34, 37, 50). The /50 and /60 band is now a shade, so a recess costs a ninth of the coverage rather than half, and each of those surfaces keeps a border or a layout edge doing the rest of the separating. /80 and /90 deliberately keep their fill: those are controls and floating fields where covering what is behind is the whole point, and the band is derived from the components so a new /70 fails a test instead of compositing to another dark rectangle.
A base plate repainting the base plate it sits in, which is not depth — it is the same colour twice. Surfaces are meant to compound: a card on a pane on the plate inside the veil should read as four things. Painting --c-bg on top of --c-bg is not one of them, and it costs nothing to get wrong while both are opaque. The graph gutter carried bg-background three lines below the pane that already carried it, so it composited 8.2% of the desktop against the commit list's 14.1% beside it and read as a hard rectangle in the middle of the glass — the same complaint as before, one layer further in. A nested base plate is now transparent, except where the nesting is an occluder: a sticky, absolute or fixed element sharing its parent's colour is covering content that scrolls under it, not repainting the ground, and the diff's sticky gutter is exactly that. The exemption list is derived from the components rather than written down, so a new occluder idiom fails a test instead of quietly going see-through. The overflow fades at the gutter's edges were the same fault in gradient form: from-background is a full-alpha stop, so a translucent pane had two opaque bands at its edges, and Tailwind's to-transparent is rgb(0 0 0 / 0), so the ramp travelled through black and fringed on light themes. .gp-edge-fade owns both ends and fades a shade on macOS, since the gutter deliberately no longer has a colour to fade from.
Translucency that stopped at the chrome, which reads as broken rather than as a decision. The glass classes covered the title bar, sidebar, status bar and workspace plate; the commit composer, branch cards, detail panes and every other ordinary panel painted through three bare Tailwind utilities that compile to alpha 1, so the result was glass wrapped around opaque rectangles. There are 439 such call sites and editing them is not a fix, it is 439 chances to miss one — the tokens are the owner, and [class~=...] overrides the bare utility only, so bg-surface/60 keeps the alpha its author chose. Hover and selection stay dense on purpose: they are a difference against what they sit on, and thinning both sides equally erases it.
The graph gutter was a hard rectangle in the middle of a glass pane, and CSS could not fix it. A 2D context with an opaque backing store paints every pixel it owns. Underneath was a conflation: GraphTheme.background served both as the colour to fill the surface with and as the colour to punch node haloes with — the same job only while the canvas is opaque, because over glass there is no colour that equals "what is behind", and a colour-matched disc stops being a hole and becomes a dark blob. The two are now separate: --bg-main resolving to transparent puts the canvas and its strip cache on an alpha backing store, clears each frame instead of filling it (an alpha surface that is merely not filled keeps the last frame and smears as it scrolls), and cuts haloes with destination-out.
Every macOS dialog was paying for a second blur that could not see anything. A backdrop-filter makes its element a backdrop root, so the full-viewport blurred scrim left the card inside it sampling the scrim's own flat wash instead of the application — a card-sized filter pass per dialog whose only input was a solid colour, and one that renders identically to a filter that works. All eight dialog scrims now route through a shared .gp-scrim, which on macOS drops its own filter so the card is the only blurred surface and blurs the real application behind it. Probed with an invert(1) test page in Chromium 148: a translateZ(0), isolation, contain: layout paint or will-change: transform ancestor passes the backdrop through, while backdrop-filter, opacity < 1 and filter cut it off.
Four always-on backdrop filters ran on every composited frame for a difference nobody can point at. The title bar, repository strip, sidebar and status bar are laid out beside and above the content panes and never over them, so the only thing in their backdrop is the shell's own smooth gradient — and blurring a smooth gradient returns the same smooth gradient. Chrome now carries translucency and colour but no filter, with its saturation baked into the field's colours instead; menus, popovers, toasts and dialog cards, which genuinely sit over commit rows and diffs, are the only tier still filtered. Nothing is filtered at all until one of them is on screen.
The workspace pane stayed opaque while the chrome around it turned to glass, which is why the result read as a dark application with translucent edges rather than a translucent application. It is three quarters of the window. It now carries the same veil, and no backdrop filter, for the reason above.
Thinning the surfaces handed the contrast to whatever was behind them. Once the field — and then the desktop — becomes the ground under 11px muted text, --c-text-muted is measured against a colour the design does not own. --mac-shell-veil keeps the shell at partial opacity so the worst case is bounded rather than unknown, the dark blobs are deep and saturated rather than pastel because chroma reads as glass without raising luminance, and the light theme dilutes the field and thickens the fill because dark-on-white has less headroom than light-on-black. Measured on the running app over a pure white full-screen backdrop: muted text held 4.74:1 – 7.23:1, body text 10.5:1 or better. An earlier iteration that passed dark at 4.9:1 was dropping light to 4.3:1.
A command whose output could not be read reported that its output was too large.BoundedRun.truncated was one boolean meaning two unrelated things — stdout hit the byte budget, and the drain thread never delivered inside its two-second grace window — and every caller that turns a prefix into a sentence could only name one of them. They named the wrong one. A git for-each-ref whose entire output was 1,482 bytes reached a user's diagnostics log as output exceeded 64 MB, on a repository with 27 refs. Nine surfaces asserted that cause: git_timeout, capture_command (every non-git tool), three provenance readers, the terminal's truncation strip, the diff viewer's banner, the AI explanation's "this commit's diff exceeded the read budget" warning, and Pulse's "log output hit the payload budget". The reason now travels with the prefix as Incomplete::{OverCap, Unread}, rendered by one subject-free describe() that each surface prefixes with its own subject, and carried over the wire on DiffPayload, PulseReport and TerminalRunResult. The coverage banner, whose flag has three sources that are not all caps, drops the invented cause instead of naming one. The engine already kept the two apart one frame earlier and then threw the distinction away.
A test that could only fail for the wrong reason.a_fast_command_returns_well_inside_the_old_quantum asserted a no-op command finished in under 200 ms end to end — a wall-clock claim about a machine the test does not own, so a busy host failed it for reasons unrelated to the property, and it was measured failing exactly that way. It also never protected the property it named: reintroducing the flat 15 ms poll tick it exists to catch costs ~33 ms end to end and passes the 200 ms budget comfortably (verified by injecting the regression). It now measures run_bounded against the same machine's own bare spawn-and-reap, best-of-12 alternating samples: contention inflates both sides together, the two minima match to the nanosecond with every core saturated, and the injected flat tick fails it with a message naming the cause.
The commit graph filed true statements about your repositories in the crash log. History parked outside branches, remotes and tags — agent turn checkpoints in refs/cmux, refs/codex, refs/cline — is disclosed rather than silently dropped, which is right; it was disclosed only through the diagnostics ring. graphStore also carried it in state.warnings, and nothing in the UI ever read that field, so a repository fact arrived looking like a malfunction, once per repository per launch, and once more each time a harness wrote another checkpoint (the sentence embeds a commit count, so 36 → 37 defeats the dedupe). The payload now separates warnings (a probe that failed — someone has to fix it) from notices (what the graph deliberately does not draw, and how to change that), and the notices render above the graph they are about, next to a link to the setting they name. A workspace of five agent-driven repositories went from 23 diagnostics entries to 1.
readOnlyHint: true was false.gitpulse_insights, gitpulse_status, gitpulse_ledger_events and gitpulse_change_context each created .devcouncil/ledger.sqlite — with its -wal and -shm — inside a fresh checkout, and repository_status additionally ran the legacy row migration. MCP clients gate user approval on exactly that annotation. Reads now go through non-creating variants (ledger::status_readonly, ledger::tail_readonly, bindings::repository_status_readonly), a binding lookup short-circuits when there is no ledger to hold one, and the ledger comes into existence when the first event is recorded. The desktop keeps the creating path. A derived test walks the whole catalog and fails if any tool leaves a file behind.
Code intelligence reported "I could not answer" as "the answer is none". Every devmap-queryUnavailable { reason } — not indexed, could not be parsed, no traversal start, no path — was funnelled through a constructor that hardcodes available: true, reason: None, so a typo'd or newly added symbol returned total: 0, items: []: byte for byte what a symbol with genuinely no callers returns. This repository's own instructions tell agents to check the blast radius before editing and trust the result. The engine's reason is now carried through, the six entry points validate repo_path through the same gate every other repository surface uses ("" resolved against the server's own working directory and answered about whatever repository it was started in), and a blank query argument is refused rather than answered.
Every tool input is validated against its declared schema. Each tool advertised additionalProperties: false and typed properties, and nothing checked them: "limit": "500" reached as_u64(), produced None, and silently became the default of 200 — a request answered with different work than it asked for, reported as success. 4294967296 truncated through as u32 to 0. Unknown arguments were ignored. The spec makes this a MUST; a schema-subset validator now enforces it, numeric arguments carry real bounds, and a keyword the validator does not understand fails the build rather than passing unchecked.
The stdio transport survived nothing. Measured against the previous read loop: one invalid UTF-8 byte ended it and the process exited 0, so the client recorded a clean shutdown and every queued request behind that byte was discarded; 600 MiB of newline-free input took the process from 10 MiB to 616 MiB resident; a well-formed message that failed struct validation was answered id: null with a parse error, so the client's pending entry never resolved; a discarded broken-pipe error left a server executing requests and spawning git for a client that had gone; and one slow call blocked every other request for as long as it ran. Frames are now read bounded through a shared framer the harness sidecar also uses, a bad frame is answered and skipped, requests run concurrently under a deadline and a panic guard, and closing stdin drains in-flight work rather than discarding it. Twenty adversarial integration tests cover it, including structure-aware and raw-byte fuzzing.
gitpulse_provenance reported maximum freshness for an empty commit.commit_sha: "" built the range ..HEAD, which git resolves to HEAD..HEAD, yielding distance: 0, confidence: 1.0, is_fresh: true. The field's own doc comment says it was made Option<u32> precisely so an unmeasurable commit could not claim that.
gitpulse_task_view claimed it had read a store it could not open. A corrupt state.sqlite returned available: true, leases: [] — the machine-readable statement "I read it; there are no active leases".
gitpulse_ledger_events returned the oldest events and a fabricated total. The cursor was pinned at 0 against an ORDER BY id ASC query, so a tool described as reading history returned the fifty oldest events ever recorded, with no way to reach recent ones; total was the returned count wearing the name of the history size. It now reports returned and truncated for what they are.
Insights folded "not measured" into "measured zero". A worktree whose git status failed, or one past the scan cap, has dirty_files: null and was counted clean in the dirty aggregate; a failed agent listing rendered as "no agent sessions running"; a collision scan that failed on some worktrees still reported ok: true with those worktrees counted as neither scanned nor unscanned; ChangeContext had no failure channel at all, so four of its five probes failed into empty values that read as "nothing in flight"; per-row churn warnings were dropped, so an unreadable diff read as a file with no changes. Each now carries its own coverage, and snapshot has the aggregate deadline its cheaper sibling already had.
worktree_path could name a completely unrelated repository.gitpulse_active_changes and gitpulse_change_context never related it to repo_path, so B's files came back stamped with A's identity. Both now go through the same family check the ledger already used.
Desktop external links, default app launching, and directory revealing were dead at runtime.capabilities/default.json granted opener:allow-default-urls, which defined scopes for http/https/mailto/tel but left commands.allow empty, causing open_url to fail with "Command plugin:opener|open_url not allowed by ACL". open_path and reveal_item_in_dir were ungranted as well, with errors silently ignored in catch blocks or unhandled promises. Furthermore, raw path concatenation in file viewers bypassed containment checks. Opening and revealing files are now routed through a dedicated backend command (desktop::shell) that proves path containment against the canonicalized repository root, refusing path traversal and escaping symlinks. The frontend ACL grant is narrowed to open_url only. Dual contracts (acl_contract.rs, openExternal.test.ts) and a 32,000-case stress test (shell_gate_stress.rs) verify that frontend invocations and declared capabilities match.
Six accuracy defects on the Health view, where bounded results claimed complete coverage or obscured failure causes.
Vulnerability breakdown: formatAuditCounts omitted info from its category list and parameter type, so findings counted into total silently disappeared from the breakdown.
Unreferenced symbols: CodeintelResponse carries total, shown, and truncated; all three were dropped on arrival, so a query stopped early by token limits reported "No unreferenced symbols" — an all-clear from a partial scan.
Direct vulnerability filter: printed an arbitrary floor threshold as a total count.
Dependabot status: rendered the same empty blank for "checked, nothing open" and "never checked", turning an unexamined remote state into an implied all-clear.
Local audit failures: an installed scanner that errored displayed a generic "Local audit incomplete" with no cause named. The panel now identifies the failing scanner and surfaces the error reason.
Ecosystem breakdown: printed slice(0, 4) without cap disclosure, concealing backend family limits. HealthPanel.test.ts derives count honesty rules directly from source, and health-failure-codes-contract verifies backend failure codes match the frontend scanner map.
Catastrophic regex backtracking (ReDoS) in line search and file queries. The catastrophic-backtracking guard admitted patterns with bounded repetition ((?:\w|(\w+){2,4}){3}, taking 5.0 s at 26 characters during fuzzing), ambiguous alternation ((a|a)*, (?:aa|a)*), and variable-length group bodies ((aa?)*), which explode exponentially on hostile inputs. The backtracking detector (hasUnboundedNesting) now checks for repetition multipliers ($\ge 2$), overlapping branch openings, and varying matched lengths. Adversarial fuzzing (redos.stress.test.ts) verifies that admitted patterns evaluate within generous millisecond budgets on hostile inputs.
File tree filtering now honestly discloses partial matches when hitting time budgets. Path filtering under complex queries or large file trees runs under a wall-clock time budget to prevent freezing the window. Previously, when a scan hit its time budget, the partial prefix was displayed as the full result. FileTreePanel and fileQuery now propagate a truncated flag end-to-end: the Explorer count displays ≥N files in warning amber, the tooltip explains that a partial count is shown, and an explicit notice prompts the user to narrow the pattern.
Recent repository popup menu overflow and keyboard navigation. The recent repositories menu in RepoTabBar.svelte could overflow the viewport. The menu now enforces height constraints (max-h-[min(28rem,calc(100vh-7.5rem))], max-w-[calc(100vw-1rem)], overflow-y-auto, and overscroll-contain), and both opening and arrow-key navigation scroll focused items into view (scrollIntoView({ block: "nearest" })).
Streaming numstat rename synchronization and bounded network git operations. Git numstat parsing in git_reader.rs properly synchronizes brace-escaped and quoted rename paths. Network-facing git operations enforce execution deadlines and process group cleanup to eliminate orphaned background processes.
Logging truncation safety on UTF-8 char boundaries. Truncation in logger formatting could cut across multi-byte character sequences; truncation is now strictly aligned to valid character boundaries.
Performance
completion/complete: 12.4 ms → 1.1 µs. It spawned git worktree list on every keystroke. The listing is now reused for two seconds, which collapses a typing burst into one spawn.
A rejected tool call: 40.9 µs → 1.5 µs. Validating one argument rebuilt the entire 11.5 KB tool catalog to find one schema; the catalog is immutable and is now built once.
codeintel::status no longer materialises the whole edge table to count it. It called latest_edges(0.0) and threw the rows away through .len() — 46 MB resident on a 13-file repository — on a call that runs on every gitpulse_status and every gitpulse_insights.
cargo bench --bench mcp_protocol reports these; it is deliberately outside cargo test, because a wall-clock assertion that fails on a loaded machine is worse than no check.
Internal
The app version is guarded as single-sourced from package.json. Contract tests previously asserted hardcoded version strings (manifest.version === "0.0.5"), which passed when written but drifted or tested stale versions after release bumps. scripts/version-source-contract.test.ts scans all source, test, and CI workflow files to ensure no source file hardcodes the active version literal, requiring tests to derive it dynamically from appVersion(), and verifies that all release manifests discovered in the tree match.
The macOS universal build shipped a binary list that had gone stale, and the release died at bundling. Tauri's universal-apple-darwin build lipos only mainBinaryName, while its macOS bundler copies every [[bin]] declared in Cargo.toml into the .app; scripts/bin/lipo closes that gap by piggy-backing on the main binary's lipo. Its helper list was written down when there were two auxiliary binaries, and gitpulse-hook — added later — never joined it, so the bundler failed with gitpulse-hook ... does not exist, naming the missing file rather than the stale list. No local build could see it: a plain tauri build targets one architecture, so every binary lands in the same directory and the shim is never needed. The list is now derived from Cargo.toml, a binary the bundler will ask for but that was never built now fails loudly instead of being skipped in silence, and scripts/lipo-shim-contract.test.ts drives the shim with a recorder to prove every declared [[bin]] gets stitched.
macos-private-api was declared for every platform but configured for only one, so Linux and Windows could not build. On each platform it builds, tauri-build re-derives the Cargo features the merged config implies and aborts when they differ from the ones declared on the [dependencies] tauri entry. macOSPrivateApi: true lived in tauri.macos.conf.json, so off macOS the base config implied no features while Cargo.toml still asked for macos-private-api: the v0.0.6 pre-flight died on Linux with "remove the macos-private-api feature" before a single platform started building, and a macOS ci:local run could not see it because the host config agreed. The key now sits in the base tauri.conf.json, where it holds everywhere; the feature is a no-op off macOS, whose guards all read any(not(target_os = "macos"), feature = "macos-private-api"). Target-scoping the manifest instead cannot work — tauri-build reads the first dependency table naming the crate and stops. scripts/mac-material-contract.test.ts now re-derives that comparison for each config permutation, so a feature-implying key placed in a platform override fails locally in milliseconds instead of on a release runner.
Windows and macOS CI treated a check that could not run as a product failure. Source-contract tests read files via a file URL's pathname property, which is not a filesystem path on Windows. The MCP doctor test assumed POSIX execute bits, which Node's X_OK ignores on Windows. Go coverage planner tests assumed go is on PATH; macos-26 only keeps it in the tool cache. Unix-only real_repo helpers were still compiled on Windows, so clippy -D warnings failed once Vitest started passing. The Codex plugin-path test required a /plugins/gitpulse suffix, which a Windows \\?\ path does not have. The portable-path contract now catches the split pathname form, the doctor test injects access, Go command-shape tests force the toolchain present so a missing runtime is explained rather than failed, those helpers are gated #[cfg(unix)], and the plugin-root assertion compares path components.