From cda8554c538c873d86d048ff5bb84606f519e886 Mon Sep 17 00:00:00 2001 From: hellno Date: Sun, 7 Jun 2026 21:40:08 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20agentic-engineering=20scaffold=20?= =?UTF-8?q?=E2=80=94=20lints,=20fmt,=20supply-chain,=20CI=20&=20agent=20do?= =?UTF-8?q?cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapt deckard PR#8's general-purpose Rust guardrails to the single-crate 0BSD Deck starter and extend them to close a coding agent's OODA loop: - Manifest lint policy ([lints.rust]/[lints.clippy]: deny todo/dbg!/unused_must_use/ unsafe_code) so rust-analyzer + cargo check show the exact rules CI enforces. - clippy.toml, rustfmt.toml (deterministic, stable-keys-only), and a fully-seeded deny.toml supply-chain gate (advisories/bans/licenses/sources all green). - CI hardening: --locked everywhere, fmt --check, clippy on the default config too, cargo test (CI never ran the 6 command_palette tests), non-blocking cargo-deny job, concurrency-cancel. - CLAUDE.md + AGENTS.md with a Definition of Done, and docs/AGENTIC-ENGINEERING.md rationale; wallet-specific deckard hardening deliberately omitted. - OODA closers: `just ci`/`just fix`, .vscode/.zed clippy-on-save (editor==CI), .editorconfig, and --message-format guidance. Verified: fmt clean, clippy green on both feature configs, 6/6 tests, cargo deny green. --- .editorconfig | 21 ++ .github/workflows/ci.yml | 54 +++++- .vscode/settings.json | 5 + .zed/settings.json | 12 ++ AGENTS.md | 74 +++++++ CLAUDE.md | 74 +++++++ Cargo.toml | 20 ++ clippy.toml | 20 ++ deny.toml | 108 +++++++++++ docs/AGENTIC-ENGINEERING.md | 373 ++++++++++++++++++++++++++++++++++++ justfile | 23 ++- rustfmt.toml | 9 + 12 files changed, 784 insertions(+), 9 deletions(-) create mode 100644 .editorconfig create mode 100644 .vscode/settings.json create mode 100644 .zed/settings.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 clippy.toml create mode 100644 deny.toml create mode 100644 docs/AGENTIC-ENGINEERING.md create mode 100644 rustfmt.toml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..498833d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# Deterministic whitespace/EOL/charset for the files rustfmt does NOT touch (YAML, TOML, +# Markdown). Keeps an agent's edits to ci.yml / Cargo.toml / docs from producing whitespace-only +# diffs that bury the real change. Honored by most editors + editor-integrated agents. +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.rs] +max_line_length = 100 + +[*.{yml,yaml,toml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 953e069..8428026 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,13 @@ on: env: CARGO_TERM_COLOR: always +# Cancel a still-running CI for a branch when a newer commit is pushed. Deck's first +# git-gpui build is slow, so when an agent pushes a fix-up the stale run is killed and +# the runner starts on the latest commit immediately (faster feedback, fewer minutes). +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: macos: runs-on: macos-latest @@ -30,9 +37,18 @@ jobs: # source of truth. rustup (preinstalled on GitHub runners) auto-installs the # pinned version and its clippy/rustfmt components on the first cargo call. - uses: Swatinem/rust-cache@v2 - - run: cargo build - - run: cargo build --features tray - - run: cargo clippy --all-targets --features tray -- -D warnings + # --locked everywhere: reproducibility lives in the committed Cargo.lock (it + # pins exact git gpui commits). --locked fails CI on a stale lock; `just + # bump-gpui` rewrites+commits the lock, so this never fights the bump flow. + - run: cargo build --locked + - run: cargo build --locked --features tray + - run: cargo clippy --locked --all-targets --features tray -- -D warnings + # why: lint the DEFAULT/shipping feature config too — CI only ever linted + # the tray build, so a clippy regression on the default build slipped through. + - run: cargo clippy --locked --all-targets -- -D warnings + # why: run the test suite — the command_palette fuzzy-match tests existed but + # CI never executed them, so an agent could break scoring invisibly. + - run: cargo test --locked linux: runs-on: ubuntu-latest @@ -55,6 +71,32 @@ jobs: libfontconfig1-dev libfreetype6-dev \ libssl-dev \ libgtk-3-dev libayatana-appindicator3-dev libxdo-dev - - run: cargo build - - run: cargo build --features tray - - run: cargo clippy --all-targets --features tray -- -D warnings + # why: formatting is OS-independent, so gate `cargo fmt --check` ONCE on the + # cheaper Linux runner instead of the 10x-billed macOS one. Deck is already + # fmt-clean, so this lands green with no baseline-format commit. + - run: cargo fmt --all --check + # --locked everywhere: reproducibility lives in the committed Cargo.lock (it + # pins exact git gpui commits). --locked fails CI on a stale lock; `just + # bump-gpui` rewrites+commits the lock, so this never fights the bump flow. + - run: cargo build --locked + - run: cargo build --locked --features tray + - run: cargo clippy --locked --all-targets --features tray -- -D warnings + # why: lint the DEFAULT/shipping feature config too — CI only ever linted + # the tray build, so a clippy regression on the default build slipped through. + - run: cargo clippy --locked --all-targets -- -D warnings + # why: run the test suite — the command_palette fuzzy-match tests existed but + # CI never executed them, so an agent could break scoring invisibly. + - run: cargo test --locked + + # why: supply-chain gate (advisories / bans / sources / licenses) via cargo-deny. + # deny.toml is already seeded and passes locally (`cargo deny check` is green). This lands + # NON-BLOCKING (continue-on-error) only so the first CI run can't surprise-break a PR — flip it + # to a required check (drop continue-on-error) after one green run on CI. + cargo-deny: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check advisories bans sources licenses diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..19fefba --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.check.command": "clippy", + "rust-analyzer.check.allTargets": true, + "rust-analyzer.checkOnSave": true +} diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..9503236 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,12 @@ +{ + "lsp": { + "rust-analyzer": { + "initialization_options": { + "check": { + "command": "clippy", + "allTargets": true + } + } + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1e2b53b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# AGENTS.md + +Guidance for coding agents (Codex and others) working in Deck. Claude Code reads +the identical rules in `CLAUDE.md` — the two files are kept in lockstep. The +*why* behind every lint and gate lives in `docs/AGENTIC-ENGINEERING.md`. + +## What Deck is + +Deck is a native desktop-app **starter** built on GPUI + gpui-component in Rust +(macOS + Linux, rendered on Metal/wgpu). It is meant to be **forked, renamed, +and shipped** as your own app — there is no domain logic to preserve, just a +clean, working foundation. + +It is a single crate (package `deck`, license `0BSD`, edition 2021). The git +GPUI stack is pinned in `Cargo.lock`; the toolchain is pinned in +`rust-toolchain.toml` to match Zed's gpui build — do not change either casually. + +### `src/` layout + +- `main.rs` — entry point; opens the window and wires up the app. +- `shell.rs` — the single root view; owns persisted `Settings` and routing. +- `command_palette.rs` — the ⌘K (Ctrl K) launcher, with fuzzy-match tests. +- `welcome.rs` — the Welcome page (a centered card). +- `settings.rs` — typed preferences persisted to the platform config dir. +- `settings_view.rs` — the Settings page rendered by `Shell`. +- `theme.rs` — dark/light palette with a selectable brand accent. +- `tray.rs` — optional menu-bar tray icon + dock hiding (`--features tray`). + +## The loop (verify and self-correct without a CI round-trip) + +- Type-check fast with `cargo check`. The full app build pulls GPUI from git, so + the **first** build is slow; it is cached after that. Run with `just run` + (`just run-tray` for the menu-bar variant). +- **`just ci`** runs the entire Definition of Done in one command (fmt-check + + clippy on both feature configs + tests) — the same gate as CI, so green here + means green there. Run it before declaring a change done. +- **`just fix`** auto-applies clippy's machine-fixable suggestions and formats. + Loop `just fix` → `just ci` to self-correct. +- For self-correction, prefer machine-readable diagnostics: + `cargo clippy --message-format=short` (one line each) or `--message-format=json` + (structured spans + applicable fixes). The default human format is for people. +- Editor == CI: `.vscode/` and `.zed/` set rust-analyzer to check with **clippy**, + so in-editor warnings match the CI lint set (rust-analyzer otherwise runs plain + `cargo check` and would miss the `[lints.clippy]` rules). + +## Definition of Done + +All four must hold before you call a change done — `just ci` checks them at once. +**Paste the command output as evidence; never claim done while anything is red.** + +1. `cargo fmt --all --check` is clean. +2. clippy `-D warnings` is green on **both** the default build **and** + `--features tray` (`just check`). +3. `cargo test` is green (the `command_palette` fuzzy-match tests live here). +4. No new or changed deps in `Cargo.toml` / `Cargo.lock` unless explicitly + approved. The git GPUI stack is bumped **only** via `just bump-gpui` — + never hand-edit those pins. + +## Code constraints + +These mirror the manifest lints; internalize them before writing code: + +- No `todo!()` and no `dbg!()` — both are denied. +- Never ignore a `Result` — `unused_must_use` is denied (a dropped fallible + IO/serde result is a silent bug). Propagate with `?` or handle it. +- `unsafe_code` is denied crate-wide. A genuinely necessary `unsafe` block needs + a reviewed `// SAFETY:` comment plus a scoped `#[allow(unsafe_code)]`. +- Deck is an app, so `.expect()` on genuinely-infallible GPUI handles is fine + (see `main.rs` and `tray.rs`). Prefer `Result`/`?` everywhere else. + +## Design work + +Ground UI changes in `README.md` and `docs/LEARNINGS.md` (real screenshots and +hard-won GPUI lessons) — not remembered descriptions of how things look. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ec12d6e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +Guidance for Claude Code (and any agent reading this file) working in Deck. +Keep this in lockstep with `AGENTS.md` — they are the same rules. The *why* +behind every lint and gate lives in `docs/AGENTIC-ENGINEERING.md`. + +## What Deck is + +Deck is a native desktop-app **starter** built on GPUI + gpui-component in Rust +(macOS + Linux, rendered on Metal/wgpu). It is meant to be **forked, renamed, +and shipped** as your own app — there is no domain logic to preserve, just a +clean, working foundation. + +It is a single crate (package `deck`, license `0BSD`, edition 2021). The git +GPUI stack is pinned in `Cargo.lock`; the toolchain is pinned in +`rust-toolchain.toml` to match Zed's gpui build — do not change either casually. + +### `src/` layout + +- `main.rs` — entry point; opens the window and wires up the app. +- `shell.rs` — the single root view; owns persisted `Settings` and routing. +- `command_palette.rs` — the ⌘K (Ctrl K) launcher, with fuzzy-match tests. +- `welcome.rs` — the Welcome page (a centered card). +- `settings.rs` — typed preferences persisted to the platform config dir. +- `settings_view.rs` — the Settings page rendered by `Shell`. +- `theme.rs` — dark/light palette with a selectable brand accent. +- `tray.rs` — optional menu-bar tray icon + dock hiding (`--features tray`). + +## The loop (verify and self-correct without a CI round-trip) + +- Type-check fast with `cargo check`. The full app build pulls GPUI from git, so + the **first** build is slow; it is cached after that. Run with `just run` + (`just run-tray` for the menu-bar variant). +- **`just ci`** runs the entire Definition of Done in one command (fmt-check + + clippy on both feature configs + tests) — the same gate as CI, so green here + means green there. Run it before declaring a change done. +- **`just fix`** auto-applies clippy's machine-fixable suggestions and formats. + Loop `just fix` → `just ci` to self-correct. +- For self-correction, prefer machine-readable diagnostics: + `cargo clippy --message-format=short` (one line each) or `--message-format=json` + (structured spans + applicable fixes). The default human format is for people. +- Editor == CI: `.vscode/` and `.zed/` set rust-analyzer to check with **clippy**, + so in-editor warnings match the CI lint set (rust-analyzer otherwise runs plain + `cargo check` and would miss the `[lints.clippy]` rules). + +## Definition of Done + +All four must hold before you call a change done — `just ci` checks them at once. +**Paste the command output as evidence; never claim done while anything is red.** + +1. `cargo fmt --all --check` is clean. +2. clippy `-D warnings` is green on **both** the default build **and** + `--features tray` (`just check`). +3. `cargo test` is green (the `command_palette` fuzzy-match tests live here). +4. No new or changed deps in `Cargo.toml` / `Cargo.lock` unless explicitly + approved. The git GPUI stack is bumped **only** via `just bump-gpui` — + never hand-edit those pins. + +## Code constraints + +These mirror the manifest lints; internalize them before writing code: + +- No `todo!()` and no `dbg!()` — both are denied. +- Never ignore a `Result` — `unused_must_use` is denied (a dropped fallible + IO/serde result is a silent bug). Propagate with `?` or handle it. +- `unsafe_code` is denied crate-wide. A genuinely necessary `unsafe` block needs + a reviewed `// SAFETY:` comment plus a scoped `#[allow(unsafe_code)]`. +- Deck is an app, so `.expect()` on genuinely-infallible GPUI handles is fine + (see `main.rs` and `tray.rs`). Prefer `Result`/`?` everywhere else. + +## Design work + +Ground UI changes in `README.md` and `docs/LEARNINGS.md` (real screenshots and +hard-won GPUI lessons) — not remembered descriptions of how things look. diff --git a/Cargo.toml b/Cargo.toml index 0c98ce0..405d5fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,26 @@ default-run = "deck" name = "deck" path = "src/main.rs" +# Lint policy lives in the manifest (not just CI flags) so rust-analyzer and `cargo check` surface +# the same rules CI enforces — zero feedback latency for an agent. Deck is a SINGLE crate (no +# [workspace]), so these go directly under the package as [lints.rust] / [lints.clippy] — NOT +# [workspace.lints] and NOT `[lints] workspace = true` (that indirection is only for multi-crate +# workspaces). Rationale + the deliberately-rejected lints: docs/AGENTIC-ENGINEERING.md. +[lints.rust] +unused_must_use = "deny" # an ignored Result from a fallible IO/serde call is a silent bug +# deny, NOT forbid: Deck has zero unsafe today; deny still compiles and leaves a reviewed escape +# hatch (// SAFETY: + scoped #[allow(unsafe_code)]) if a future objc2 bump needs raw unsafe in the +# tray path. forbid cannot be locally overridden, so it would brick that build with no way out. +unsafe_code = "deny" + +[lints.clippy] +# priority = -1 is REQUIRED on a lint *group* so the individual lints below can still override it. +# warn here (not deny) + `-D warnings` in CI is the reth/alloy convention: a toolchain bump that +# adds new clippy lints then can't brick local `cargo build` — only CI goes red. +all = { level = "warn", priority = -1 } +todo = "deny" # a stray todo!() left on a code path panics in production +dbg_macro = "deny" # dbg! debug noise left behind by an agent + [dependencies] # Fresh GPUI, straight from Zed's git — the DEFAULT channel (Metal on macOS, wgpu on # Linux). `gpui-component` is developed against Zed's gpui HEAD, so the only way to pair diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..4ab09b1 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,20 @@ +# clippy knobs for Deck. why these (and why the wallet stuff is omitted): docs/AGENTIC-ENGINEERING.md + +msrv = "1.95.0" # match rust-toolchain.toml + +# Deck is an APP, and the command_palette tests unwrap/expect/panic freely — keep them legal. +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true + +# doc-valid-idents: only matters once you turn on clippy::pedantic (doc_markdown). Deck enables +# only clippy::all, which does NOT include doc_markdown, so this key is unnecessary today. +# uncomment + add YOUR domain idents (proper nouns clippy would otherwise flag) if you go pedantic: +# doc-valid-idents = ["GPUI", "macOS", "WebGPU", ".."] + +# disallowed-methods is the highest-signal clippy tool: a compile error WITH a printed reason. +# Deck has no footguns to ban out of the box (the wallet originals — mem::forget / thread_rng — +# are key-zeroize concerns, irrelevant here). ban your own once you find one: +# disallowed-methods = [ +# { path = "std::env::set_var", reason = "mutates global env; pass config explicitly" }, +# ] diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..35c4812 --- /dev/null +++ b/deny.toml @@ -0,0 +1,108 @@ +# cargo-deny — supply-chain gate for Deck (advisories / licenses / bans / sources). +# +# Run locally: +# cargo install cargo-deny +# cargo deny check +# +# Why this file looks the way it does (the rationale, per-knob): docs/AGENTIC-ENGINEERING.md §4. +# +# Deck is a single-crate GPUI + Rust desktop-app starter (no [workspace]). Its own +# crate is licensed 0BSD, so cargo-deny — which license-checks first-party crates too — +# must allow 0BSD in the main list below. A scoped [licenses] exceptions block (further +# down) whitelists GPL for ONLY three named Zed crates the git gpui stack drags in — see +# the ⚠️ note there before shipping a closed-source fork. + +# --------------------------------------------------------------------------- +# Advisories — RustSec security (vulnerability) + unmaintained-crate advisories. +# --------------------------------------------------------------------------- +[advisories] +# Security VULNERABILITY advisories are denied by default everywhere — that hard gate stays. +# The softer "unmaintained" class is scoped to "workspace": flag an unmaintained crate only when +# Deck depends on it DIRECTLY (something a maintainer can act on), not when it's buried in the git +# gpui stack (async-std, instant, paste, rustls-pemfile, … — upstream's to fix and unavoidable +# here). A real vulnerability anywhere in the tree still fails the build. +unmaintained = "workspace" +yanked = "deny" +ignore = [] # add { id = "RUSTSEC-…", reason = "…" } to silence a specific advisory, with justification + +# --------------------------------------------------------------------------- +# Licenses — allow a permissive superset. Every dependency (and Deck's own 0BSD +# crate) must resolve to one of these. +# +# This list is SEEDED and currently passes (`cargo deny check licenses` is green against the +# committed Cargo.lock). The git gpui stack is large and licenses drift between bumps, so re-run +# `cargo deny check licenses` after every `just bump-gpui` and reconcile any new entries. +# --------------------------------------------------------------------------- +[licenses] +version = 2 +confidence-threshold = 0.9 +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "0BSD", # Deck's own crate — BSD Zero Clause, zero-attribution, fork-friendly + "ISC", + "Unicode-3.0", + "Zlib", + "MPL-2.0", + "Unlicense", + "CC0-1.0", + "NCSA", # libfuzzer-sys — permissive (U. of Illinois/NCSA), pulled transitively + "bzip2-1.0.6", # libbz2-rs-sys — permissive BSD-style bzip2 license +] + +# ⚠️ COPYLEFT IN THE TREE — read this before shipping a closed-source fork. Zed's logging/tracing +# crates (zlog, ztracing, ztracing_macro) are GPL-3.0-or-later and are LINKED into the binary as a +# normal runtime dep: gpui -> sum_tree -> ztracing -> zlog. This is inherent to building on Zed's +# git gpui stack — Deck did not add it. The exceptions below allow GPL ONLY for those three named +# crates (so the gate can go green); they do NOT open the whole tree to copyleft. If your product +# cannot take a GPL-3.0 obligation, raise it with upstream gpui — don't just delete these lines and +# ship. Re-verify the crate list after every `just bump-gpui`. +exceptions = [ + { allow = ["GPL-3.0-or-later"], crate = "zlog" }, + { allow = ["GPL-3.0-or-later"], crate = "ztracing" }, + { allow = ["GPL-3.0-or-later"], crate = "ztracing_macro" }, +] + +# --------------------------------------------------------------------------- +# Bans — duplicate versions, wildcard requirements, and explicitly-denied crates. +# --------------------------------------------------------------------------- +[bans] +# warn, not deny: the git gpui stack legitimately pulls in multiple versions of +# some crates (e.g. objc2 0.5 + 0.6), so a hard deny would be permanently red. +# Surface the duplicates as a warning instead of blocking on them. +multiple-versions = "warn" +# allow, not deny: Deck's gpui stack (gpui, gpui_platform, gpui-component, gpui-component-assets) +# is pulled from git WITHOUT version requirements — reproducibility comes from the pinned +# Cargo.lock, not semver. cargo-deny reads those unversioned git deps as wildcards, so `deny` is +# permanently red on Deck's OWN deps (allow-wildcard-paths only covers path deps on unpublished +# crates, not git deps). The "no new deps without approval" rule in CLAUDE.md is the human gate +# against a stray crates.io `*`. +wildcards = "allow" +deny = [ + { crate = "openssl", reason = "prefer rustls/ring; avoid OpenSSL CVE surface" }, +] + +# --------------------------------------------------------------------------- +# Sources — only crates.io and the known git origins are allowed. +# +# allow-git below is the SIX git origins Deck currently depends on (the Zed gpui +# stack + gpui-component), written in the EXACT form Cargo.lock records them — note +# the `.git` suffix on reqwest and wgpu, which Zed pins that way. (cargo-deny happens +# to normalize the suffix away when matching, but the exact form is match-proof if that +# ever changes.) Re-derive after every `just bump-gpui` — Zed may add or retire +# forks — with: grep "git+" Cargo.lock +# --------------------------------------------------------------------------- +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-git = [ + "https://github.com/zed-industries/zed", + "https://github.com/zed-industries/font-kit", + "https://github.com/zed-industries/reqwest.git", + "https://github.com/zed-industries/scap", + "https://github.com/zed-industries/wgpu.git", + "https://github.com/longbridge/gpui-component", +] diff --git a/docs/AGENTIC-ENGINEERING.md b/docs/AGENTIC-ENGINEERING.md new file mode 100644 index 0000000..2a98c16 --- /dev/null +++ b/docs/AGENTIC-ENGINEERING.md @@ -0,0 +1,373 @@ +# Agentic engineering: lints, constraints & CI for building fast with coding agents + +> **Audience:** anyone who forks **Deck** to build their own native GPUI app, and wants a coding +> agent (or a hurried human) to ship without quietly breaking things. This is the *why* behind +> Deck's lint/CI config; the *what to run* lives in `CLAUDE.md` / `AGENTS.md`. Deck is a +> **single-crate** starter (no `[workspace]`), so everything here is package-level — there is no +> `[workspace.lints]` indirection to set up. + +## The thesis + +A coding agent is fast but has no taste and no memory of *this* repo's intent. The cheapest way to +make an agent reliably produce good code is to **make the compiler and CI say no for you.** Every +rule below converts a class of mistake from "caught in review, maybe" into "caught at `cargo check`, +always." + +Three principles, in priority order: + +1. **The manifest is the source of truth, not CI flags.** If the lint policy only lives in a CI + `-D warnings` flag, the agent doesn't see it until the build is already red. Put it in + `Cargo.toml` (`[lints.rust]` / `[lints.clippy]`) and `clippy.toml` so `rust-analyzer` and + `cargo check` show the *exact same* policy at the moment code is written. **Shorten the feedback + loop to zero.** +2. **CI is the only gate that matters.** Pre-commit hooks, editor warnings, and good intentions are + all bypassable (`git commit --no-verify`, "I'll fix it later"). An agent will confidently report + "done" while red. So anything you actually care about must *block merge* in CI. +3. **Prefer compile-time over runtime, and `warn` → fix → `deny` over big-bang.** Land a new lint as + `warn`, clear the backlog, *then* flip to `deny`. A rule that breaks `main` on day one gets + reverted; a rule that lands green stays forever. + +These match what the paradigm Rust projects do — the source research cross-checked +[reth](https://github.com/paradigmxyz/reth), [alloy](https://github.com/alloy-rs/alloy), +[Zed](https://github.com/zed-industries/zed) (Deck's GPUI source), tokio, ripgrep, and the +[Embark Studios shared lint set](https://github.com/EmbarkStudios/rust-ecosystem/blob/main/lints.rs). +The recurring pattern is identical: a lint table in the manifest, a `clippy.toml`, a checked-in +`rustfmt.toml`, `cargo-deny` in CI, and a CI matrix that runs fmt + clippy + test on every push. + +--- + +## Tier 1 — the do-now set + +Each item: **what**, **why it helps an agent**, the **snippet**, and the **tradeoff**. + +### 1. Put the lint policy in the manifest — `[lints]` in `Cargo.toml` + +**What.** Deck is a single crate, so the lint policy goes **directly** in `Cargo.toml` under +`[lints.rust]` and `[lints.clippy]`. (There is no `[workspace.lints]` table and no +`[lints] workspace = true` line — that two-table indirection is only for multi-crate *workspaces*. +For a single crate it would be wrong.) Today Deck's `-D warnings` policy exists *only* as a CLI flag +in `just check` and CI. + +```toml +# Cargo.toml — directly under the package, single-crate style +[lints.rust] +unused_must_use = "deny" # an ignored Result from a fallible IO/serde call is a silent bug +unsafe_code = "deny" # deny (not forbid) — see §6 for why the escape hatch matters + +[lints.clippy] +all = { level = "warn", priority = -1 } # priority = -1 is required on a lint *group* +todo = "deny" # a stray todo!() left on a code path panics in production +dbg_macro = "deny" # dbg!(x) debug noise left behind by an agent +``` + +**Why for an agent.** The agent now sees `clippy::all` and the deny-list in-editor and at +`cargo check`, identical to what CI enforces — no more "looked fine locally, red in CI." `todo` / +`dbg_macro` at `deny` make two classic agent habits (leaving a `todo!()` stub, leaving a `dbg!`) +into hard compile errors. All four deny-level items are pre-verified clean against Deck's `src/` +today (no `todo!`/`dbg!`/`unsafe` anywhere, and no ignored `Result`s), so this lands green. + +**Tradeoff.** `clippy::all` stays at `warn` in the manifest (not `deny`) while CI keeps +`-D warnings`. That's deliberate: `warn` lets you iterate locally without every style nit blocking +`cargo build`, while CI still fails on any warning. Setting `clippy::all = "deny"` would make a +*future toolchain bump* (new clippy lints) break local builds for unrelated reasons. `warn` + CI +gate is the reth/alloy convention. + +### 2. A project lint config — `clippy.toml` + +**What.** New file at the repo root. + +```toml +msrv = "1.95.0" # match rust-toolchain.toml; governs which API suggestions clippy makes +allow-unwrap-in-tests = true # the command_palette fuzzy-match tests unwrap() freely +allow-expect-in-tests = true +allow-panic-in-tests = true + +# Deck only enables clippy::all, which does NOT include doc_markdown, so doc-valid-idents is +# unnecessary today. If you later turn on clippy::pedantic, add your own domain jargon here so +# doc_markdown stops flagging it, e.g.: +# doc-valid-idents = ["..", "GPUI", "wgpu", "macOS"] # ".." keeps clippy's defaults + +# disallowed-methods is the highest-signal clippy tool: it turns "don't call X here" tribal +# knowledge into a compile error WITH the reason printed. Deck ships none by default; ban your +# own footguns as you discover them, e.g.: +# disallowed-methods = [ +# { path = "std::process::exit", reason = "return from main / propagate an error instead" }, +# ] +``` + +**Why for an agent.** `disallowed-methods` is the highest-signal-per-line tool clippy offers — it +prints *your* reason at `cargo check`. It's left as a commented example so a forker sees the +mechanism and can ban whatever their app must never call. The `allow-*-in-tests` keys keep Deck's +existing `command_palette` tests (which `unwrap()` scores freely) legal even if you later add +stricter crate-level restriction lints. + +**Tradeoff.** None material — the only active key is `msrv`; everything else is opt-in scaffolding. + +### 3. Deterministic formatting — `rustfmt.toml` + +**What.** New file at the repo root. All keys are **stable-channel** (Deck's pinned 1.95.0 stable +`cargo fmt` honors them; nightly-only keys like `imports_granularity` / `group_imports` are +deliberately excluded — stable silently ignores them, which is worse than not setting them). + +```toml +edition = "2021" +max_width = 100 +``` + +Intentionally minimal — `edition` + the width the code is already written to (both rustfmt defaults +today), so landing it is near-zero churn. Style idioms are left to `clippy::all`. + +**Why for an agent.** Without a checked-in config, two agents (or two rustfmt versions) format the +same code differently, producing noisy diffs that bury the real change. A pinned config makes the +diff deterministic, and `cargo fmt --all --check` in CI (§5) makes "did you run fmt?" a yes/no gate +instead of a review comment. + +**Tradeoff.** None for Deck: the repo is already `cargo fmt --check`-clean, so enabling the +`--check` gate needs **no** baseline-format commit (unlike a repo that has to reformat first). + +### 4. Supply-chain gate — `deny.toml` + a `cargo-deny` CI job + +**What.** `cargo-deny` checks the dependency tree for security advisories, banned/duplicate crates, +disallowed licenses, and untrusted sources. New `deny.toml`: + +```toml +[advisories] +yanked = "deny" +ignore = [] # add { id = "RUSTSEC-…", reason = "…" } only with written justification +# (cargo-deny v2 denies vulnerability advisories and unmaintained crates by default.) + +[licenses] +version = 2 +confidence-threshold = 0.9 +# SEED-THEN-TIGHTEN: run `cargo deny check licenses` locally and reconcile this list before +# making the CI job blocking — the git gpui stack pulls a wide license surface and a blind list +# WILL red the build. Deck's OWN crate is 0BSD (cargo-deny license-checks first-party crates too), +# which is permissive, so it's just another entry in the allow list — no exceptions block needed. +allow = ["MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", "BSD-3-Clause", + "0BSD", "ISC", "Unicode-3.0", "Zlib", "MPL-2.0", "Unlicense", "CC0-1.0"] + +[bans] +multiple-versions = "warn" # the git gpui stack legitimately duplicates crates (objc2 0.5+0.6) — deny is permanently red +wildcards = "deny" +deny = [{ crate = "openssl", reason = "prefer rustls/ring; avoid the OpenSSL CVE surface" }] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +# All SIX git origins, not two: Zed pins its own forks of font-kit/reqwest/scap/wgpu, pulled +# transitively by the gpui stack. These are the exact forms recorded in Cargo.lock (note the +# `.git` suffix on reqwest/wgpu). Re-derive with `grep 'git+' Cargo.lock` after every +# `just bump-gpui` — Zed may add or retire forks. +allow-git = ["https://github.com/zed-industries/zed", + "https://github.com/zed-industries/font-kit", + "https://github.com/zed-industries/reqwest.git", + "https://github.com/zed-industries/scap", + "https://github.com/zed-industries/wgpu.git", + "https://github.com/longbridge/gpui-component"] +``` + +**Why for an agent.** Three failure modes, gated at once: (a) pulling in a crate with a known +RUSTSEC advisory, (b) adding a dep from a random git fork — the `[sources]` allow-list permits only +the known origins (the six Zed/longbridge ones the gpui stack pulls), so a typo-squat or malicious +fork fails CI, (c) license drift. Cheap insurance for a project people fork and ship. + +**Tradeoff.** The license `allow` list must be **seeded locally first** (`cargo deny check +licenses`), and `multiple-versions` must stay `warn` because the git gpui tree legitimately +duplicates crates. So the CI job lands **non-blocking** and is promoted after one green run (see +Rollout). + +### 5. Close the CI gaps + +**What.** Deck's CI currently runs only `cargo build`, `cargo build --features tray`, and +`cargo clippy --all-targets --features tray -- -D warnings`. That means: **format is never checked, +tests never run, and clippy never lints the default (`--features`-less) build that Deck actually +ships.** That last one matters most for agents: there are 6 real `#[test]` fns in +`src/command_palette.rs` (fuzzy-match scoring) that **CI never runs today** — an agent can break +the scoring and CI stays green. Add (split across the existing macOS/Linux jobs to respect the +macOS minute multiplier): + +```yaml +# linux job (formatting is OS-independent, so check it once on the cheap runner): +- run: cargo fmt --all --check +- run: cargo clippy --locked --all-targets -- -D warnings # the DEFAULT feature config CI never linted +- run: cargo test --locked + +# macOS job: +- run: cargo clippy --locked --all-targets -- -D warnings +- run: cargo test --locked +``` + +> Add `--locked` to **every** `cargo build`/`clippy`/`test` invocation: reproducibility lives +> entirely in the committed `Cargo.lock` (it pins the exact git gpui commits), so `--locked` makes a +> stale lockfile a CI failure. `just bump-gpui` rewrites and commits the lock, so this never fights +> the bump workflow. + +Plus the `cargo-deny` job (non-blocking first): + +```yaml +cargo-deny: + runs-on: ubuntu-latest + # Informational on first land. Promote to a required check after one green run AND after seeding + # deny.toml [licenses].allow from a local `cargo deny check licenses`. + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: { command: check advisories bans sources licenses } +``` + +**Why for an agent.** This is principle #2 made real. Running `cargo test` and the default-feature +clippy closes the "green CI but actually broken" hole that lets an agent honestly believe it's done. + +**Tradeoff.** Slightly longer CI; the macOS runner is 10× billed on *private* repos (Deck is +public, so free) — hence formatting and `cargo-deny` run only on Linux. Preserve the existing CI +structure when adding these: the macOS cost note, the Linux system-deps apt block, the +rust-toolchain-as-source-of-truth note, `Swatinem/rust-cache`, and `env: CARGO_TERM_COLOR`. + +### 6. Unsafe policy — deny, with a reviewed escape hatch + +**What.** + +```toml +# Cargo.toml [lints.rust] +unsafe_code = "deny" # deny, NOT forbid — see below +``` + +**Why for an agent.** Deck has **zero** `unsafe` in `src/` today. The only Apple-FFI in the whole +tree is the tray feature's dock-hiding via `objc2`, and it goes through *safe* `objc2` wrappers — so +`unsafe_code = "deny"` compiles today for **both** the default and `--features tray` builds. Denying +it means no agent can quietly introduce a raw `unsafe {}` block without it showing up as a compile +error. + +**Tradeoff — why `deny` not `forbid`:** `forbid` cannot be locally overridden. If a future `objc2` +bump reintroduces a raw `unsafe {}` block in the tray path, `forbid` would brick the build with no +escape hatch. `deny` lets you add a single, reviewed, `// SAFETY:`-commented `#[allow(unsafe_code)]` +at that exact site. (Note: `[lints]` governs first-party code only — `unsafe` inside dependencies is +unaffected, which is correct.) + +### 7. Tell the agent the rules — `CLAUDE.md` + `AGENTS.md` + +**What.** Deck has neither file today; create both. `CLAUDE.md` is what Claude Code reads; +`AGENTS.md` is what Codex and other agents read. Keep them in lockstep and point both here for the +rationale. The essentials: the fast iteration command, an explicit **definition of done**, and the +code constraints (in prose, mirroring the lints). + +```markdown +## Engineering & verification +Iterate fast: `cargo check` for a quick type-check loop. The full app build pulls gpui from git, so +the FIRST build is slow (cached after). +Definition of done (ALL must hold; paste the command output as evidence — never claim done while red): +1. `cargo fmt --all --check` clean +2. `just check` green — clippy `-D warnings` on BOTH the default and `--features tray` configs +3. `cargo test` green +4. No new/changed deps (Cargo.toml or Cargo.lock) unless explicitly approved; bump the git gpui + stack ONLY via `just bump-gpui`, never hand-edit those pins. + +## Code constraints +`todo!`/`dbg!` denied; ignored Results (`unused_must_use`) denied; `unsafe_code` denied (a new +unsafe block needs a reviewed `// SAFETY:` comment + a scoped `#[allow(unsafe_code)]`). Deck is an +app, so `.expect()` on genuinely-infallible GPUI handles is fine (see `main.rs` / `tray.rs`) — +prefer `Result`/`?` everywhere else. +``` + +**Why for an agent.** An explicit "definition of done with evidence" is the single most effective +guardrail against an agent declaring victory while red. The constraints duplicate what the lints +enforce, in prose, so the agent internalizes them *before* writing rather than learning from a +failed build. + +**Tradeoff.** Two files to keep in sync. Keep them short (~40–55 lines) and point both here. + +> **Design pointer for UI work:** ground changes in `README.md` and `docs/LEARNINGS.md` (real +> screenshots and hard-won GPUI lessons), not in remembered descriptions of how GPUI behaves. + +### 8. Close the local OODA loop — `just ci` / `just fix`, editor == CI, parseable diagnostics + +**What.** Everything above gives the agent *rules*; this gives it the *loop* — the ability to +observe a verdict and self-correct without a CI round-trip. Five small pieces: + +```makefile +# justfile — one command that IS the Definition of Done, plus one-shot auto-remediation +ci: + cargo fmt --all --check + cargo clippy --locked --all-targets -- -D warnings + cargo clippy --locked --all-targets --features tray -- -D warnings + cargo test --locked +fix: + cargo clippy --fix --allow-dirty --allow-staged --all-targets + cargo clippy --fix --allow-dirty --allow-staged --all-targets --features tray + cargo fmt +``` + +- **`just ci`** runs the exact CI gate locally. Before this, `just check` ran clippy *only* (no + fmt-check, no tests) — so an agent that ran it, saw green, and reported "done" was still red in CI + on formatting or a broken test. One command now collapses the whole Observe step into a single + pass/fail. +- **`just fix`** auto-applies clippy's machine-fixable suggestions and reformats — the correct Act + when `just ci` reports a fixable nit. The agent loops `fix` → `ci` instead of hand-editing. +- **`.vscode/settings.json` + `.zed/settings.json`** set rust-analyzer's check command to `clippy`. + This is the precondition for principle #1: rust-analyzer defaults to plain `cargo check`, which + does **not** run the `[lints.clippy]` rules — so without this the in-editor signal would silently + diverge from CI. (Deck is itself a Zed app, so the `.zed/` config is doubly apt.) +- **`--message-format=short` / `=json`** — tell the agent (in `CLAUDE.md`/`AGENTS.md`) to read + diagnostics in machine-readable form. `short` is one line per diagnostic; `json` carries + structured spans and applicable fixes. Far more robust for an agent than scraping rendered + carets/ANSI. +- **`.editorconfig` + CI `concurrency` cancel** — deterministic whitespace for the files rustfmt + doesn't touch (YAML/TOML/Markdown), and cancellation of superseded CI runs so a fix-up push gets + a verdict on the *latest* commit instead of queueing behind a stale slow build. + +**Why for an agent.** This is the difference between an agent that needs a human (or a 15-minute CI +round-trip) to learn it's wrong, and one that runs `just ci`, reads structured failures, runs +`just fix`, and converges on its own. It is the most direct lever on "run longer, unattended." + +**Tradeoff.** A few lines of `justfile` + two tiny editor configs; `just ci` duplicates the CI step +list, so keep the two in sync (they're short). All of it is zero new dependencies. + +--- + +## Deliberately NOT recommended (so we don't over-rotate) + +| Tempting | Why we skip it | +|---|---| +| `#![forbid(unsafe_code)]` at the crate root | No escape hatch if an `objc2` bump needs a raw `unsafe` in the tray path. Use `unsafe_code = "deny"` in the manifest instead (§6). | +| `clippy::pedantic` / `clippy::nursery` globally | Too noisy for a small UI app; nursery lints are unstable → a toolchain bump can surface new warnings and break `-D warnings`. Cherry-pick instead. | +| The full `clippy::restriction` group | Clippy's own docs say never enable it wholesale (it contains mutually contradictory lints). Pick individual ones. | +| Edition 2024 bump | Deck pins 1.95.0 in lockstep with gpui's git HEAD; an edition bump is orthogonal churn that risks the gpui pairing. | +| Nightly rustfmt keys / nightly build flags (`-Zthreads`, cranelift, `-Zshare-generics`) | The 1.95.0 stable pin is **mandatory** for the git gpui build — nightly flags in a committed config brick `cargo build` for everyone. | +| `panic = "abort"` | Deck's `command_palette` tests rely on unwinding; `abort` also loses the backtrace on a panic. Keep the default unwinding. | +| `multiple-versions = "deny"` in deny.toml | The git gpui stack pulls duplicate versions (objc2 0.5+0.6); permanently red. Keep `warn`. | +| mold/lld linker config | Not worth the per-machine setup churn for a starter: it forces every forker/CI runner to install and configure an alternate linker, and on macOS `lld` is a measured *regression* vs Apple's default `ld`/`ld-prime`. The stock toolchain links fine. Do nothing. | +| deckard-core-style crate-level `#![deny(clippy::unwrap_used, expect_used, panic, indexing_slicing)]` + `#![forbid(unsafe_code)]` on a security core | Deck has **no** security/crypto core — that hardening is wallet-specific and intentionally not applied here (e.g. `expect_used` would red `src/main.rs` and `src/tray.rs`, where expecting an infallible GPUI handle is correct). If you fork Deck into something security-sensitive, add a headless, GPUI-free core crate and adopt that hardening there. See the [deckard PR](https://github.com/hellno/deckard/pull/8) for the full pattern (bounds-checked reader, `Zeroizing` secrets, `#[must_use]` on secret types). | + +--- + +## Rollout order (stays green at every step) + +1. **Formatting.** Add `rustfmt.toml`. Deck is already fmt-clean, so `--check` passes immediately — + no baseline-format commit needed. +2. **Lints.** Add `[lints.rust]` / `[lints.clippy]` to `Cargo.toml` + `clippy.toml`. Run clippy + (both feature configs); the deny-level items are pre-verified clean. +3. **Unsafe policy.** `unsafe_code = "deny"` in the manifest (compiles for default and `--features tray`). +4. **CI gaps.** Add fmt-check, default-feature clippy, and `cargo test` (with `--locked`). Land, + confirm green, mark required. +5. **deny.toml.** Seed `[licenses].allow` from a local `cargo deny check licenses` run *first*; land + the `cargo-deny` job non-blocking; promote to required after one green run. +6. **Docs.** Add `CLAUDE.md` and `AGENTS.md`. +7. **Local loop.** Add the `just ci` / `just fix` recipes, `.vscode//.zed/` clippy-on-save, + `.editorconfig`, and the CI `concurrency` cancel. All green by construction (no code change). + +The invariant: **every new lint enters as `warn`; every new CI job enters non-blocking. Confirm +green, then tighten.** `main` is never red because of a hardening change. + +--- + +## Provenance + +Adapted for Deck from the [deckard PR #8](https://github.com/hellno/deckard/pull/8) multi-agent +research sweep of paradigm Rust OSS repos (reth, alloy, Zed, tokio, ripgrep, Embark), plus a +feasibility pass that verified each recommendation against real source. The wallet-specific rules +from that work (a `#![forbid(unsafe_code)]` security core, `unwrap_used`/`expect_used`/`panic`/ +`indexing_slicing` denies, `Zeroizing` secrets, the bounds-checked reader, crypto `doc-valid-idents`, +and `mem::forget`/`thread_rng` `disallowed-methods`) are deliberately **dropped** here — Deck is a +fork-it-rename-it GPUI starter, not a security-sensitive app. What remains is the general-purpose +core that applies to any GPUI + Rust project. diff --git a/justfile b/justfile index f66d239..1dc4b39 100644 --- a/justfile +++ b/justfile @@ -17,12 +17,29 @@ run-release: run-tray: cargo run --features tray -# Format + lint (both feature configurations). +# Format the code. fmt: cargo fmt + +# Lint both feature configurations (clippy, warnings = errors). For the FULL gate use `just ci`. check: - cargo clippy --all-targets -- -D warnings - cargo clippy --all-targets --features tray -- -D warnings + cargo clippy --locked --all-targets -- -D warnings + cargo clippy --locked --all-targets --features tray -- -D warnings + +# The full CI gate, locally — the whole Definition of Done in one command. Run this before you +# call a change done; it mirrors .github/workflows/ci.yml so green here == green in CI. +ci: + cargo fmt --all --check + cargo clippy --locked --all-targets -- -D warnings + cargo clippy --locked --all-targets --features tray -- -D warnings + cargo test --locked + +# Auto-fix everything fixable: clippy's machine-applicable suggestions + formatting. +# Re-run `just ci` afterwards to confirm green. (--allow-dirty so it works mid-edit.) +fix: + cargo clippy --fix --allow-dirty --allow-staged --all-targets + cargo clippy --fix --allow-dirty --allow-staged --all-targets --features tray + cargo fmt # Bump the git GPUI stack to the latest upstream commits, then rebuild. # Reproducibility lives in Cargo.lock — commit it (and rust-toolchain.toml if you diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b778998 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,9 @@ +# rustfmt for Deck. intentionally minimal = near-zero churn; Deck is already fmt-clean so +# enabling the CI `cargo fmt --check` gate needs no baseline-format commit. + +edition = "2021" +max_width = 100 + +# STABLE-channel keys ONLY. The pinned 1.95.0 stable `cargo fmt` SILENTLY ignores nightly-only +# keys (imports_granularity, group_imports, etc.) — setting them gives a false sense of config +# that never runs, which is worse than not setting them at all. don't add nightly keys here.