From c927ac1c6e041f96326e4a32e76ca13da8f6f5be Mon Sep 17 00:00:00 2001 From: main Date: Tue, 31 Mar 2026 14:21:52 -0400 Subject: Initial import --- .gitignore | 2 + AGENTS.md | 16 + docs/bootstrap-fresh.md | 128 +++++++ docs/bootstrap-retrofit.md | 156 +++++++++ docs/rust-linting-proposal.md | 537 +++++++++++++++++++++++++++++ skills/exterminate-slop/SKILL.md | 472 +++++++++++++++++++++++++ skills/exterminate-slop/agents/openai.yaml | 4 + skills/rust-bootstrap/SKILL.md | 53 +++ skills/rust-bootstrap/agents/openai.yaml | 4 + template/fresh/Cargo.lock | 7 + template/fresh/Cargo.toml | 118 +++++++ template/fresh/check.py | 225 ++++++++++++ template/fresh/clippy.toml | 5 + template/fresh/crates/app/Cargo.toml | 9 + template/fresh/crates/app/src/main.rs | 3 + template/fresh/rust-toolchain.toml | 4 + 16 files changed, 1743 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 docs/bootstrap-fresh.md create mode 100644 docs/bootstrap-retrofit.md create mode 100644 docs/rust-linting-proposal.md create mode 100644 skills/exterminate-slop/SKILL.md create mode 100644 skills/exterminate-slop/agents/openai.yaml create mode 100644 skills/rust-bootstrap/SKILL.md create mode 100644 skills/rust-bootstrap/agents/openai.yaml create mode 100644 template/fresh/Cargo.lock create mode 100644 template/fresh/Cargo.toml create mode 100644 template/fresh/check.py create mode 100644 template/fresh/clippy.toml create mode 100644 template/fresh/crates/app/Cargo.toml create mode 100644 template/fresh/crates/app/src/main.rs create mode 100644 template/fresh/rust-toolchain.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46670be --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +target/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9ea771b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +This repo is the source of truth for bootstrapping or ratcheting Rust lint posture. + +Choose exactly one surface: + +- Fresh bootstrap: read [docs/bootstrap-fresh.md](/home/main/programming/projects/rust_starter/docs/bootstrap-fresh.md) and adapt the files under [template/fresh](/home/main/programming/projects/rust_starter/template/fresh). +- Retrofit: read [docs/bootstrap-retrofit.md](/home/main/programming/projects/rust_starter/docs/bootstrap-retrofit.md), then diff the target repo against [template/fresh](/home/main/programming/projects/rust_starter/template/fresh) and [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md). + +House rules: + +- Keep lint semantics in root `Cargo.toml`; keep runners, CI, and editor config orchestration-only. +- Make every member crate opt into `[lints] workspace = true`. +- Pin an exact stable toolchain in `rust-toolchain.toml`. +- Prefer `#[expect(..., reason = "...")]` for temporary local suppressions. +- Retrofit is ratchet work: preserve stricter local policy and justified local carve-outs; do not blindly paste the template. + +Use [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md) for rationale and policy details, not as a paste target. diff --git a/docs/bootstrap-fresh.md b/docs/bootstrap-fresh.md new file mode 100644 index 0000000..f77b819 --- /dev/null +++ b/docs/bootstrap-fresh.md @@ -0,0 +1,128 @@ +# Fresh Bootstrap + +Use this surface when the target repo is blank or when you are setting up Rust linting from scratch. + +## Goal + +Install the `rust-starter` lint posture cleanly: + +- root `Cargo.toml` owns lint levels +- every crate inherits workspace lint policy +- `rust-toolchain.toml` is pinned exactly +- `clippy.toml` stays tiny and only carries structured config +- the runner stays thin and reads canonicalization and verification command vectors plus the source-file cap from workspace metadata instead of restating policy + +## Inputs To Set Deliberately + +Before copying the template, decide: + +- actual workspace members +- package naming +- license policy +- exact pinned stable toolchain +- actual MSRV / `rust-version` +- whether tests should allow `expect`, `unwrap`, or `panic` +- whether the repo is library-heavy enough to keep rustdoc linting in the fast gate or only the deep gate +- whether unsafe Rust is forbidden or tightly-governed +- whether the default `2500`-line source-file cap should stay as-is or be tightened for this repo + +## Files To Start From + +Use these as the concrete baseline: + +- [template/fresh/Cargo.toml](/home/main/programming/projects/rust_starter/template/fresh/Cargo.toml) +- [template/fresh/rust-toolchain.toml](/home/main/programming/projects/rust_starter/template/fresh/rust-toolchain.toml) +- [template/fresh/clippy.toml](/home/main/programming/projects/rust_starter/template/fresh/clippy.toml) +- [template/fresh/check.py](/home/main/programming/projects/rust_starter/template/fresh/check.py) +- [template/fresh/crates/app/Cargo.toml](/home/main/programming/projects/rust_starter/template/fresh/crates/app/Cargo.toml) +- [template/fresh/crates/app/src/main.rs](/home/main/programming/projects/rust_starter/template/fresh/crates/app/src/main.rs) + +Do not copy them mechanically. Replace the placeholder members, names, license, and version/toolchain values. + +## Sequence + +### 1. Write the root manifest first + +Start from the template root manifest and adapt: + +- workspace member list +- package metadata +- pinned `rust-version` +- repo-wide Clippy carve-outs + +Keep the command vectors in `[workspace.metadata.rust-starter]` even if you later wrap them with a different local runner. + +The fresh template exports an ordered `canonicalize_commands` pipeline. Keep it repo-owned in the root manifest and make the default `check` path run it automatically before the verification gate so humans and agents do not need to remember a separate pre-pass. + +Also keep the source-file cap in `[workspace.metadata.rust-starter.source_files]`. The starter default is a hard `2500`-line limit across `*.rs` files. Tighten it if the repo already has sharper discipline. + +### 2. Pin the toolchain second + +Write `rust-toolchain.toml` early, before deciding whether a lint is noisy or broken. Lint output is part of the toolchain contract. + +### 3. Make every crate inherit lint policy + +Each member crate must contain: + +```toml +[lints] +workspace = true +``` + +Do not rely on memory or convention here; add it explicitly. + +### 4. Add `clippy.toml` only if it carries real configuration + +The default template only relaxes `expect`, `unwrap`, and `panic` inside tests. If the target repo does not want that, delete the file instead of leaving inert config behind. + +Do not move global allow/deny policy into `clippy.toml`. + +### 5. Add a thin runner + +The template `check.py` is intentionally small. It reads canonicalization and verification commands plus the source-file policy from workspace metadata, enforces the file cap, and then runs the Rust commands. + +Default runner shape: + +- `check`: enforce the file cap, run canonicalization, then verify formatting, lint, and tests +- `verify`: enforce the file cap without mutating files; this is the non-writing CI-friendly path +- `deep`: `check` plus docs +- `fix` / `canon`: canonicalization only + +If the target repo already prefers another orchestration surface, keep that surface only if it remains policy-thin: + +- okay: `xtask`, `just`, shell wrapper, CI job that calls the canonical commands +- not okay: restating dozens of `-A` and `-D` flags in scripts + +If the repo has checked-in generated Rust that should not count against the cap, express that with explicit `exclude` patterns under `[workspace.metadata.rust-starter.source_files]` instead of weakening the global limit. + +### 6. Keep the deep gate opt-in + +The template only installs the fast gate. Add the deeper posture when the repo is mature enough: + +- `cargo hack clippy --workspace --all-targets --feature-powerset` +- `cargo hack test --workspace --feature-powerset` +- `cargo doc --workspace --all-features --no-deps` +- `cargo deny check` +- optionally `cargo nextest` and `cargo semver-checks` + +## Do Not Do These + +- do not use `--no-deps` as the workspace-wide Clippy default +- do not enable `clippy::restriction` wholesale +- do not enable `clippy::nursery` wholesale +- do not duplicate lint policy in scripts, CI, and editor config +- do not leave placeholder toolchain or MSRV values unreviewed + +## Acceptance Checklist + +- root `Cargo.toml` contains `[workspace.lints.rust]`, `[workspace.lints.rustdoc]`, and `[workspace.lints.clippy]` +- root `Cargo.toml` contains canonical command vectors in `[workspace.metadata.rust-starter]` +- root `Cargo.toml` contains an ordered `canonicalize_commands` pipeline in `[workspace.metadata.rust-starter]` +- root `Cargo.toml` contains `[workspace.metadata.rust-starter.source_files]` with an intentional `max_lines` value +- each member crate opts into `[lints] workspace = true` +- `rust-toolchain.toml` pins an exact stable patch and includes `clippy` plus `rustfmt` +- `clippy.toml` is either tiny and justified or absent +- the default `check` path canonicalizes before verification, and a non-mutating `verify` path exists for CI or drift detection +- no runner or CI file duplicates the Clippy allowlist + +Read [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md) only when you need rationale or policy nuance. diff --git a/docs/bootstrap-retrofit.md b/docs/bootstrap-retrofit.md new file mode 100644 index 0000000..c07be56 --- /dev/null +++ b/docs/bootstrap-retrofit.md @@ -0,0 +1,156 @@ +# Retrofit + +Use this surface when the target repo already has Rust linting, but you want to tighten the ratchet without clobbering valid local structure. + +## Goal + +Migrate the repo toward the `rust-starter` posture while staying diff-aware: + +- preserve stricter existing rules +- preserve justified local carve-outs +- delete duplicated policy only after it has been re-homed into the manifest +- never paste the fresh template wholesale over a living repo +- add a hard source-file line cap without erasing stricter local limits or justified generated-code exemptions + +## First Pass: Inventory The Existing Surfaces + +Before editing, inspect all of the places the repo may already encode lint policy: + +- root and member `Cargo.toml` +- `check.py`, `xtask`, shell wrappers, `justfile`, or other runners +- CI workflow files +- any existing auto-fix or canonicalization commands +- `clippy.toml` +- `rust-toolchain.toml` +- editor settings +- existing oversized source files, checked-in generated code, and any current file-length checks + +You are trying to answer two questions: + +1. What policy already exists? +2. Where is that policy duplicated? + +## Comparison Baseline + +Use these as the baseline, not as paste targets: + +- [template/fresh](/home/main/programming/projects/rust_starter/template/fresh) +- [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md) + +The fresh template tells you the shape to converge toward. The proposal doc tells you why. + +## Rules Of Engagement + +### Preserve stricter local policy + +If the repo already does something stricter than the template, keep it unless there is a clear repo-specific reason to relax it. + +Examples: + +- stricter rustdoc policy +- additional Clippy bans +- a more demanding deep gate +- an intentionally tighter unsafe policy +- a lower source-file line cap + +### Preserve justified local exceptions + +If the repo has exceptions with clear local justification, do not erase them just because the template lacks them. + +Instead: + +- move them into root `Cargo.toml` if they are repo-wide +- keep them local if they are truly local +- add or preserve comments / `reason = "..."` +- use `workspace.metadata.rust-starter.source_files.exclude` for checked-in generated Rust or other deliberate file-cap carve-outs + +### Remove duplicate policy, not local intent + +When a repo encodes the same allowlist in both `Cargo.toml` and a runner script, the script copy should die. + +But only remove the script copy after the equivalent manifest-owned policy exists and the runner still executes the same effective gate. + +### Treat `clippy.toml` as configuration-only + +If the repo uses `clippy.toml` to hold the main allow/deny architecture, migrate that policy into `[workspace.lints.clippy]`. + +Keep `clippy.toml` only for structured knobs such as test allowances. + +## Ratchet Order + +Apply tightenings in this order: + +### 1. Pin or refresh the toolchain + +Add `rust-toolchain.toml` if missing. If present but floating, pin it. + +### 2. Install workspace lint tables + +Move repo-wide policy into root `[workspace.lints.*]`. + +### 3. Make member crates inherit explicitly + +Add `[lints] workspace = true` to every member crate. + +### 4. Re-home script flags into the manifest + +Take inline `cargo clippy -- -A/-D ...` tails from scripts and CI, and migrate them into the root manifest in grouped, commented form. + +### 5. Tighten local suppression discipline + +Prefer: + +- `#[expect(..., reason = "...")]` for temporary or evidence-backed suppressions +- `#[allow(..., reason = "...")]` only for stable local policy + +### 6. Simplify the runner + +Once the manifest is authoritative, collapse the runner to orchestration-only plus generic manifest-backed checks such as the source-file cap. + +If the repo already has an auto-fix pass, re-home it into root `workspace.metadata.rust-starter.canonicalize_commands` and make the default local `check` path invoke it before the verification gate instead of relying on engineers or agents to remember a separate pre-pass. + +### 7. Install the source-file cap + +Add `[workspace.metadata.rust-starter.source_files]` to the root manifest and set `max_lines` deliberately. + +Default to `2500` if the repo has no existing stance. If the repo already enforces a stricter cap, keep the stricter value. If the repo has checked-in generated Rust that would make the rule meaningless, exclude those paths explicitly instead of disabling the whole mechanism. + +### 8. Add deep-gate posture if the repo is ready + +Add `cargo hack`, docs, and dependency-hygiene checks only when the repo can support them without turning the whole effort into churn theater. + +## Special Cases + +### Existing `expect_used = "allow"` + +Do not keep this globally just because the repo had it historically. Check whether the real need is test-only ergonomics. If so: + +- set `expect_used = "deny"` in the root manifest +- move the relaxation into `clippy.toml` via `allow-expect-in-tests = true` + +### Existing repo-specific carve-outs + +If the repo carries exceptions for domain-heavy code such as geometry, parsing, or numerics, expect some of them to stay. The goal is not zero exceptions. The goal is explicit, centralized, justified exceptions. + +### Checked-in giant files + +If the repo already contains Rust files over the default cap, do not blindly raise the limit to fit them. Decide whether each file should be split, exempted, or accepted behind a tighter, evidence-backed local exception pattern. + +### Existing CI + +Do not rewrite CI into a second policy source. Make it call the canonical commands or the thin runner. + +If the repo wants CI to detect canonicalization drift rather than rewriting files in place, keep a non-mutating verification entrypoint such as `check.py verify` and let local `check` remain the mutating convenience path. + +## Acceptance Checklist + +- repo-wide policy lives in root `[workspace.lints.*]` +- every member crate inherits that policy explicitly +- scripts and CI no longer restate Clippy allowlists +- manifest-owned canonicalization exists where the repo wants auto-fixes, instead of living in shell aliases or tribal memory +- valid repo-specific exceptions remain intact and justified +- stricter pre-existing rules remain stricter +- the root manifest carries an intentional source-file cap and any justified exclusions +- fast gate commands still match the repo’s effective behavior + +Use [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md) when you need to justify a ratchet choice, not as a blind replacement spec. diff --git a/docs/rust-linting-proposal.md b/docs/rust-linting-proposal.md new file mode 100644 index 0000000..53b9ae2 --- /dev/null +++ b/docs/rust-linting-proposal.md @@ -0,0 +1,537 @@ +# Rust Starter Pack: Industrial-Grade Linting Proposal + +## Thesis + +The best current design is: + +1. Make the root `Cargo.toml` the single source of truth for lint levels. +2. Make every member crate opt into workspace lint inheritance with: + + ```toml + [lints] + workspace = true + ``` + +3. Keep the runner orchestration-only: source-file cap, `fmt --check`, `clippy`, tests, then optional deeper gates. +4. Pin an exact stable toolchain so lint drift is a conscious upgrade event. +5. Centralize repo-wide Clippy carve-outs and structural thresholds in the root manifest, and force local suppressions to carry reasons. + +The local corpus already converges on this. The newer `adequate_*`, `memcp`, `outpost`, and `picmash` workspaces are structurally superior to the older `check.py` / shell-script pattern because they put policy in manifest tables instead of command-line tails. + +One extra local fact matters: there is effectively no checked-in Rust CI posture in this corpus. The real gate is the repo-local checker. That makes it even more important that the checker stay a thin wrapper around manifest-owned policy instead of becoming a second policy source. + +One more distinction matters: Clippy’s `too_many_lines` is about function bodies, not whole source files. If the goal is to force large modules to split, the starter pack needs an explicit file-level check instead of pretending the existing lint already covers it. + +## Local Survey + +### The old shape + +These repos encode lint policy inside scripts: + +- [empty_status/scripts/check.py](/home/main/programming/projects/empty_status/scripts/check.py) +- [libgrid/scripts/check_lib.py](/home/main/programming/projects/libgrid/scripts/check_lib.py) +- [swarm.moe/landing/scripts/check.py](/home/main/programming/projects/swarm.moe/landing/scripts/check.py) +- [swarm.moe/tongue/scripts/rust-clippy-pedantic.sh](/home/main/programming/projects/swarm.moe/tongue/scripts/rust-clippy-pedantic.sh) +- [swarm.moe/hyperkiki/scripts/rust-clippy-pedantic.sh](/home/main/programming/projects/swarm.moe/hyperkiki/scripts/rust-clippy-pedantic.sh) + +That pattern works, but it has chronic defects: + +- the same allow/deny list gets duplicated across scripts, CI, and editor settings +- repo policy hides inside shell noise instead of living in the manifest +- inheritance is implicit rather than explicit +- the runner accumulates semantic policy instead of staying a dumb sequencer + +### The better local shape + +These repos move policy into workspace lint tables: + +- [adequate_rust_mcp/Cargo.toml](/home/main/programming/projects/adequate_rust_mcp/Cargo.toml) +- [adequate_aws_mcp/Cargo.toml](/home/main/programming/projects/adequate_aws_mcp/Cargo.toml) +- [adequate_sloccount/Cargo.toml](/home/main/programming/projects/adequate_sloccount/Cargo.toml) +- [memcp/Cargo.toml](/home/main/programming/projects/memcp/Cargo.toml) +- [swarm.moe/outpost/Cargo.toml](/home/main/programming/projects/swarm.moe/outpost/Cargo.toml) +- [picmash/Cargo.toml](/home/main/programming/projects/picmash/Cargo.toml) + +And then use thin runners such as: + +- [adequate_rust_mcp/check.py](/home/main/programming/projects/adequate_rust_mcp/check.py) +- [picmash/check.py](/home/main/programming/projects/picmash/check.py) + +This is the correct direction. The starter pack should canonize it. + +### Local convergence that looks real + +Across the stricter workspaces, the same repo-wide Clippy exceptions keep reappearing: + +- doc-detail noise: `missing_errors_doc`, `missing_panics_doc` +- threshold/style noise: `too_many_lines`, `too_many_arguments`, `items_after_statements` +- dense-symbol domains: `similar_names`, `many_single_char_names`, `module_name_repetitions`, `struct_field_names` +- numeric pragmatism: `cast_*`, `float_cmp`, `implicit_hasher` +- blanket-annotation fatigue: `must_use_candidate`, `return_self_not_must_use` +- API-shape micro-optimizations: `needless_pass_by_value`, `trivially_copy_pass_by_ref`, `ref_option`, `unused_async` + +That is not random drift anymore. It is a de facto house profile and should be made explicit. + +Just as notable is what the local corpus mostly does *not* use: + +- no `clippy.toml` as the primary home of lint levels +- no checked-in GitHub Actions workflow for Rust checks +- almost no dependency-hygiene rail beyond [empty_status/.pre-commit-config.yaml](/home/main/programming/projects/empty_status/.pre-commit-config.yaml) using `cargo-deny` and [libgrid/scripts/check_full.py](/home/main/programming/projects/libgrid/scripts/check_full.py) using `cargo machete` + +That absence is also signal. The local ecosystem has already selected manifest-owned lint policy plus a repo-local runner as the durable core. + +## What Current Upstream Supports + +### 1. Root manifest lint tables are the right source of truth + +Cargo has stable manifest lint tables, including workspace inheritance. The root workspace can define lint policy in `[workspace.lints.*]`, and each member crate must opt into it with `[lints] workspace = true`. + +Implication: + +- the starter pack should treat root `Cargo.toml` as the canonical lint policy +- forgetting `[lints] workspace = true` is a real failure mode and should be considered a policy violation + +### 2. Cargo’s own lint namespace is still unstable + +Cargo also has a distinct lint namespace under `[lints.cargo]`, but that remains nightly-only / unstable. + +Important naming collision: + +- `clippy` has a stable lint group named `cargo` +- Cargo itself has an unstable manifest namespace named `cargo` + +They are not the same thing. The starter pack should use stable `rust`, `rustdoc`, and `clippy` tables now, and avoid depending on `[lints.cargo]`. + +### 3. `clippy.toml` is for configuration knobs, not the main allow/deny architecture + +Clippy supports `clippy.toml` / `.clippy.toml`, but its configuration-file lookup is documented as unstable. That makes it the wrong place for the primary policy lattice. + +Use it only when a lint exposes configuration you genuinely want, for example: + +- `allow-expect-in-tests = true` +- `allow-unwrap-in-tests = true` +- `allow-panic-in-tests = true` + +Implication: + +- put lint levels in `Cargo.toml` +- keep `clippy.toml` tiny +- do not split the core allow/deny story across manifest tables and shell flags + +### 4. Clippy’s own guidance supports a strict but curated baseline + +Official Clippy guidance makes three things clear: + +- `pedantic` is intentionally noisy +- `restriction` is not intended to be enabled wholesale +- `nursery` is under development and unstable in spirit even when available on stable toolchains + +Inference: + +- `pedantic = deny` is fine only if paired with a curated central allowlist +- `restriction` should be cherry-picked lint by lint +- `nursery` should not be part of the default starter-pack baseline + +That matches the good local repos, except [picmash/Cargo.toml](/home/main/programming/projects/picmash/Cargo.toml), which also enables `nursery = deny`. I would keep that as an opt-in experiment profile, not the default starter profile. + +### 5. `--no-deps` is not the workspace-wide default we want + +Clippy’s usage docs are explicit here: `--no-deps` is for linting only a selected package, excluding workspace-member path dependencies, and the documented form is `cargo clippy -p example -- --no-deps`. + +That is useful for a narrow crate-local loop, but it is the wrong default for a workspace-wide industrial gate. The starter profile should lint the whole workspace, not silently skip member crates that happen to be path dependencies. + +### 6. `rust-version` is part of lint policy + +Clippy uses the crate’s MSRV configuration, and the docs note that it defaults to the `rust-version` field in `Cargo.toml`. + +Implication: + +- every starter-pack workspace should set `rust-version` +- the field should reflect the actual supported minimum, not a decorative guess + +### 7. Pinned toolchains are part of the enforcement story + +The rustup toolchain file supports exact channels, components, profile, and targets. For strict linting, that matters because Clippy output changes over time. + +Implication: + +- pin an exact stable patch release in `rust-toolchain.toml` +- include `clippy` and `rustfmt` +- prefer `profile = "minimal"` unless the repo has a real reason not to + +### 8. `unexpected_cfgs` plus `check-cfg` is one of the highest-value newer strengthenings + +Rust now supports checking custom `cfg` names and values via the `unexpected_cfgs` lint and Cargo-driven `check-cfg` configuration. + +Implication: + +- `unexpected_cfgs` belongs in the starter baseline +- any repo with custom cfgs should declare them explicitly instead of letting misspellings drift silently + +### 9. Rustdoc belongs in the linting story + +Rustdoc has its own lint namespace, including `broken_intra_doc_links` and `bare_urls`. + +Implication: + +- library-heavy repos should define `[workspace.lints.rustdoc]` +- the deeper check posture should include a docs build + +## Proposed Starter-Pack Design + +### 1. Source-of-truth layout + +Use a root `Cargo.toml` like this: + +```toml +[workspace] +members = ["crates/*"] +resolver = "3" + +[workspace.package] +edition = "2024" +rust-version = "1.xx" +version = "0.1.0" + +[workspace.lints.rust] +elided_lifetimes_in_paths = "deny" +unexpected_cfgs = "deny" +unsafe_code = "deny" +unused_crate_dependencies = "warn" +unused_lifetimes = "deny" +unused_qualifications = "deny" +unused_results = "deny" + +[workspace.lints.rustdoc] +broken_intra_doc_links = "deny" +bare_urls = "deny" + +[workspace.lints.clippy] +all = { level = "deny", priority = -2 } +pedantic = { level = "deny", priority = -1 } +cargo = { level = "warn", priority = -3 } + +dbg_macro = "deny" +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unwrap_used = "deny" +allow_attributes_without_reason = "deny" + +cargo_common_metadata = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +multiple_crate_versions = "allow" + +items_after_statements = "allow" +many_single_char_names = "allow" +match_same_arms = "allow" +module_name_repetitions = "allow" +similar_names = "allow" +struct_field_names = "allow" +too_many_arguments = "allow" +too_many_lines = "allow" +unnested_or_patterns = "allow" + +cast_lossless = "allow" +cast_possible_truncation = "allow" +cast_possible_wrap = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +float_cmp = "allow" +implicit_hasher = "allow" +manual_let_else = "allow" +map_unwrap_or = "allow" +uninlined_format_args = "allow" + +ignored_unit_patterns = "allow" +must_use_candidate = "allow" +needless_pass_by_value = "allow" +no_effect_underscore_binding = "allow" +redundant_closure_for_method_calls = "allow" +ref_option = "allow" +return_self_not_must_use = "allow" +trivially_copy_pass_by_ref = "allow" +unused_async = "allow" +used_underscore_binding = "allow" + +[workspace.metadata.rust-starter] +format_command = ["cargo", "fmt", "--all", "--check"] +clippy_command = [ + "cargo", + "clippy", + "--workspace", + "--all-targets", + "--all-features", + "--", + "-D", + "warnings", +] +test_command = ["cargo", "test", "--workspace", "--all-targets", "--all-features"] +doc_command = ["cargo", "doc", "--workspace", "--all-features", "--no-deps"] +canonicalize_commands = [ + [ + "cargo", + "fix", + "--workspace", + "--all-targets", + "--all-features", + "--allow-dirty", + "--allow-staged", + "--allow-no-vcs", + ], + [ + "cargo", + "clippy", + "--fix", + "--workspace", + "--all-targets", + "--all-features", + "--allow-dirty", + "--allow-staged", + "--allow-no-vcs", + ], + ["cargo", "fmt", "--all"], +] +``` + +And every member crate should contain: + +```toml +[lints] +workspace = true +``` + +Why this split is right: + +- `Cargo.toml` owns semantics +- `workspace.metadata` owns canonicalization and verification command vectors plus generic runner-backed structural checks for humans, CI, and tools +- any runner script becomes dumb orchestration rather than a second policy language + +Add the source-file cap alongside the command vectors: + +```toml +[workspace.metadata.rust-starter.source_files] +max_lines = 2500 +include = ["*.rs", "**/*.rs"] +exclude = [] +``` + +Why this belongs here: + +- Cargo lint tables only cover compiler and tool lints; a whole-file size cap is outside that namespace +- keeping the threshold in root metadata still makes the manifest the policy source of truth +- the runner can stay generic by enforcing a small schema instead of hard-coding repo-specific paths or numbers + +### 2. Toolchain posture + +Check in a `rust-toolchain.toml` like: + +```toml +[toolchain] +channel = "1.xx.0" +profile = "minimal" +components = ["clippy", "rustfmt"] +``` + +Use an exact stable patch at scaffold time and bump it deliberately. Do not float on `"stable"` if the goal is reproducible strictness. + +### 3. Exception policy + +This is the center of gravity. If the exception policy is wrong, the rest becomes theater. + +#### Repo-wide exceptions + +If a lint is genuinely misaligned with the repo’s design, allow it once in root `Cargo.toml`, grouped by policy cluster and commented briefly. + +Do not restate repo-wide exceptions: + +- in shell flags +- in CI YAML +- in crate roots +- in a graveyard of repeated `#[allow]`s + +#### Local exceptions + +If a lint suppression is local: + +- prefer `#[expect(..., reason = "...")]` when the exception is temporary, evidence-backed, or under review +- use `#[allow(..., reason = "...")]` only when the exception is stably intentional +- keep the scope as narrow as possible + +Because the starter profile denies `allow_attributes_without_reason`, local suppressions must explain themselves. + +#### Test code policy + +The local corpus often solves test ergonomics by setting `expect_used = "allow"` globally. I think that is too blunt. + +Better default: + +- keep production policy strict +- if the repo wants more relaxed tests, express that via tiny `clippy.toml` configuration + +Example: + +```toml +allow-expect-in-tests = true +allow-unwrap-in-tests = true +allow-panic-in-tests = true +``` + +That keeps the exception local to test code instead of weakening the whole codebase. + +#### Restriction-lint policy + +Never enable `clippy::restriction` wholesale. + +The starter baseline should cherry-pick only the restriction lints with durable operational value: + +- `dbg_macro` +- `expect_used` +- `todo` +- `unimplemented` +- `unwrap_used` +- `panic` +- `allow_attributes_without_reason` + +That is aggressive without becoming absurd. + +### 4. Fast gate versus deep gate + +#### Fast gate + +This should be the canonical local verification gate: + +1. enforce the source-file cap from `workspace.metadata.rust-starter.source_files` +2. `cargo fmt --all --check` +3. `cargo clippy --workspace --all-targets --all-features -- -D warnings` +4. `cargo test --workspace --all-targets --all-features` + +This is the default industrial baseline. + +#### Canonicalization path + +The starter pack should also export a manifest-owned canonicalization pipeline: + +1. `cargo fix --workspace --all-targets --all-features --allow-dirty --allow-staged --allow-no-vcs` +2. `cargo clippy --fix --workspace --all-targets --all-features --allow-dirty --allow-staged --allow-no-vcs` +3. `cargo fmt --all` + +The default local `check` path should run that pipeline before the verification gate so trivial rewrites do not depend on human or agent memory. + +If a repo wants CI to fail on canonicalization drift instead of rewriting files in place, expose a non-mutating `verify` path and point CI there. + +#### Deep gate + +Add a heavier gate for mature repos: + +1. `cargo hack clippy --workspace --all-targets --feature-powerset` +2. `cargo hack test --workspace --feature-powerset` +3. `cargo doc --workspace --all-features --no-deps` +4. `cargo deny check` + +If the feature graph explodes, degrade to `--each-feature` or grouped feature subsets instead of abandoning feature-matrix coverage entirely. + +### 5. Strong optional strengthenings + +#### Explicit `check-cfg` declarations + +If the repo uses custom cfgs, upgrade `unexpected_cfgs` from a scalar level to an explicit declaration: + +```toml +[workspace.lints.rust] +unexpected_cfgs = { level = "deny", check-cfg = ['cfg(tokio_unstable)', 'cfg(has_foo)'] } +``` + +This catches misspelled or drifting cfg names earlier than most teams realize. + +#### Unsafe-enabled profile + +The default profile should deny `unsafe_code`. If a repo truly needs unsafe Rust, switch from "unsafe forbidden" to "unsafe tightly governed": + +- add `unsafe_op_in_unsafe_fn = "deny"` under `[workspace.lints.rust]` +- add `undocumented_unsafe_blocks = "deny"` under `[workspace.lints.clippy]` +- review unsafe boundaries manually instead of normalizing them through blanket allowlists + +#### Editor alignment + +The rustup dev guide and rust-analyzer configuration both point toward aligning editor diagnostics with Clippy. The starter pack should recommend editor settings that make the IDE use `clippy`, all targets, and all features. + +That closes the common gap where CI is strict but the editor is only running `cargo check`. + +#### `cargo nextest` + +For real test suites, prefer `cargo nextest` in the deeper posture for speed and CI ergonomics. But keep a separate doctest command because nextest does not run doctests. + +#### `cargo semver-checks` + +If the repo publishes libraries, add `cargo semver-checks` to the deeper release gate. It is not a lint in the narrow sense, but it is exactly the kind of automated correctness pressure a serious starter pack should know about. + +#### Periodic canary job + +Run a scheduled CI job against the latest stable toolchain without updating the pinned toolchain file. Let it warn, not block merges. That gives early visibility into upcoming Clippy churn while preserving deterministic mainline CI. + +## Choices I Reject + +### Repeating `-A/-D` lists in shell or Python + +That is the old pattern. It is operationally inferior to manifest-owned policy. + +### Treating `clippy::too_many_lines` as a file-size limit + +That lint is about function length. It is not a substitute for a hard source-file cap, and pretending otherwise guarantees drift. + +### Enabling all of `clippy::restriction` + +Clippy is correct to discourage this. The group is too contradictory and too context-sensitive for a starter baseline. + +### Enabling all of `clippy::nursery` + +It is useful as a canary or opt-in experiment, not as the default industrial profile. + +### Relying on `[lints.cargo]` + +Not until Cargo’s own lint namespace stabilizes. + +### Floating toolchains in strict CI + +If the goal is "force industrial-grade linting", ambient toolchain drift is the enemy. + +## Proposed AGENTS.md One-Liner + +Use this style: + +> Set up Rust linting with the `rust-starter` industrial profile: pin an exact stable `rust-toolchain.toml`; put repo policy only in root `[workspace.lints.{rust,rustdoc,clippy}]` plus manifest-owned canonicalization and verification command vectors and source-file-cap metadata in `workspace.metadata`; make every crate opt into `[lints] workspace = true`; make the default local `check` path canonicalize first, but keep a non-mutating `verify` path for CI; cherry-pick restriction bans instead of enabling `clippy::restriction`; keep repo-wide Clippy carve-outs centralized and commented in `Cargo.toml`; and require local suppressions to use `#[expect(..., reason = "...")]` unless a permanent `#[allow(..., reason = "...")]` is truly justified. + +That is long, but it is precise and inferable by a model. + +## Bottom Line + +The starter pack should standardize the newer Cargo-centric pattern already emerging locally, harden it with toolchain pinning, distinguish manifest-owned policy from runner orchestration, export a first-class canonicalization pipeline, enforce a manifest-backed hard source-file cap, tighten suppression discipline with `reason = "..."`, and add an optional deeper gate for feature matrices, docs, dependency hygiene, and public API drift. + +The single strongest improvement over the current local state is not any one lint. It is moving the center of gravity from bespoke scripts to manifest-owned policy. + +## Sources + +Primary sources: + +- Cargo manifest `lints` section: +- Cargo workspaces and inherited lint tables: +- `cargo fix`: +- Cargo’s unstable `[lints.cargo]` namespace: +- Clippy overview and lint-group guidance: +- Clippy usage and `-D warnings`: +- Clippy configuration files: +- Clippy lint configuration options: +- Rust diagnostic attributes, including `expect` and `reason`: +- Rustdoc lints: +- Cargo-specific `check-cfg` / `unexpected_cfgs`: +- rustup toolchain file: +- rustup profiles: +- rustup dev-guide linting note: +- rust-analyzer configuration: +- `cargo deny`: +- `cargo hack`: +- `cargo nextest` features docs: +- `cargo semver-checks`: diff --git a/skills/exterminate-slop/SKILL.md b/skills/exterminate-slop/SKILL.md new file mode 100644 index 0000000..a6b572b --- /dev/null +++ b/skills/exterminate-slop/SKILL.md @@ -0,0 +1,472 @@ +--- +name: exterminate-slop +description: Perform an exhaustive Rust-only subtree audit and refactor pass for primitive obsession, under-abstraction, and DRY violations. Use when Codex needs to inspect every Rust file in a requested code subtree for strings, bools, tuples, parameter clumps, duplicated logic, repeated field bundles, or weak abstractions, and must follow a rigid ledgerized process instead of a loose best-effort review. +--- + +# Exterminate Slop + +Read the repo's [AGENTS.md](../../AGENTS.md) first. + +Rust only. Do not spend context on language-agnostic framing. + +## Contract + +- Require a concrete Rust subtree. If the user names only a repo, choose the smallest plausible subtree and state the assumption. +- Default to `audit_plus_refactor` unless the user clearly wants findings only. +- Inspect every `*.rs` file in scope. No sampling and no “representative files.” +- Create one persistent `/tmp` worklog before the first code read. Chat is summary only; the worklog is the durable source of truth. +- Partition the manifest into intentional logical cliques before deep reading. +- Read at most 8 files per wave. +- Write a `/tmp` checkpoint after every wave before reading more files. +- Record raw suspects during coverage, then promote only threshold-clearing suspects into the formal ledger after full coverage. +- Force a decision on every promoted ledger row. +- Do not edit until the promoted ledger exists, unless the user explicitly asked for a narrow hotfix instead of a full extermination pass. + +Files may be re-read. Re-reading is expected when a file belongs to multiple logical cliques or when a later duplication cluster changes the correct abstraction boundary. + +## Flow + +### 0. Create the worklog + +Create a path shaped like: + +```text +/tmp/exterminate-slop--.md +``` + +Seed it with the embedded worklog skeleton from `Embedded Forms`. + +The worklog must hold: + +- the manifest +- the clique plan +- every wave checkpoint +- the rejected clusters +- the promoted ledger +- the residual sweep + +If interrupted or resumed, reopen the same worklog before continuing. + +### 1. Lock scope and manifest + +Enumerate the full Rust-file manifest, for example with: + +```bash +rg --files -g '*.rs' +``` + +Write the manifest in the embedded form from `Embedded Forms` and persist it into the `/tmp` worklog immediately. + +### 2. Plan logical cliques + +Before deep reading, group manifest files into small logical cliques using the embedded clique-wave form from `Embedded Forms`. + +Good clique causes: + +- shared domain types +- repeated parse / validate / normalize flows +- repeated rendering / formatting flows +- repeated control-flow skeletons +- repeated construction / patch / conversion logic +- module-boundary interactions +- suspected duplication clusters + +Cliques may overlap. A file may appear in multiple cliques. Keep cliques to 2-8 files; split larger conceptual groups into multiple related waves. + +### 3. Run bounded coverage waves + +For each clique wave: + +- read at most 8 files +- use `rust_analyzer` aggressively for definitions, references, rename feasibility, and boundary understanding +- prefer `rust_analyzer` from live use sites in the current clique, not blind declaration-first jumps +- update the manifest coverage +- write a `/tmp` checkpoint containing: + - clique id and purpose + - files inspected in the wave + - raw suspects + - duplication hypotheses strengthened or weakened + - files worth re-reading in later cliques + +Do not read a 9th file until that checkpoint exists. A file counts as covered once it has been read in at least one wave. + +Concrete `rust_analyzer` pattern: + +- start at a live use site: field access, constructor call, function call, match arm, or repeated conversion site +- use hover or definition there to anchor the actual symbol in play +- then expand with references once the anchor is correct +- use rename feasibility only after the symbol has been anchored from a concrete use site + +If declaration-first references are noisy or incomplete, return to a concrete use site and trace outward again. + +### 4. Run smell passes + +Run distinct passes over the manifest and checkpointed suspects. If a pass needs more context, form another clique and respect the 8-file cap again. + +#### String suspicion + +Interrogate meaningful `String`, `&str`, string discriminators, and stringly keys. + +Ask whether the value is really an id, slug, path, URL, email, unit, currency, locale, timestamp, status, or other validated domain token; whether normalization or parsing repeats at call sites; whether the value set is finite; and whether multiple strings travel together as a latent record. + +#### Bool suspicion + +Interrogate meaningful `bool`. + +Ask whether it is really a mode, strategy, phase, policy, capability, or tri-state; whether call sites become unreadable because the meaning is positional; and whether correlated bools are encoding illegal states that want an enum or policy type. + +#### Tuple suspicion + +Interrogate tuple types, tuple returns, and repeated destructuring. + +Ask whether the order is accidental, whether the elements have stable names in human discussion, whether the tuple crosses module boundaries, and whether the same shape is repeatedly rebuilt or unpacked. + +#### Parameter-clump and field-bundle suspicion + +Interrogate functions, constructors, builders, and repeated local bundles. + +Ask whether the same 2-5 values travel together repeatedly, whether repeated bundles imply invariants, and whether an argument list wants a domain struct or value object. + +#### Duplication and under-abstraction + +Search for repeated parse / validate / normalize pipelines, repeated format / render / serialize logic, repeated match skeletons, repeated field-by-field copying, repeated conversions, repeated error shaping, and repeated query predicates. + +#### Primitive control encodings + +Interrogate string discriminators, integer type codes, sentinel values, parallel collections implying a missing domain object, and repeated `Option` / `bool` combinations that really encode richer states. + +### 5. Promote suspects + +During coverage, keep only raw suspects. After every manifest file has been inspected at least once, promote a suspect into the formal ledger only if at least one is true: + +- it appears in more than one file or module +- it crosses a crate or module boundary +- it shows invariant pressure rather than mere cosmetic repetition +- the fix is locally actionable now + +Anything not promoted must still be accounted for as a short rejected cluster using the embedded rejected-cluster form from `Embedded Forms`. Persist both promoted and rejected outcomes in the `/tmp` worklog. + +### 6. Populate and adjudicate the promoted ledger + +Populate the formal ledger with the embedded ledger form from `Embedded Forms`. Only promoted candidates belong there. + +For every row, run the embedded adjudication checklist and choose exactly one: + +- `refactor_now` +- `defer` +- `false_positive` +- `needs_broader_design` + +Keep the authoritative ledger in the `/tmp` worklog even if chat shows only a condensed view. + +### 7. Refactor in batches + +Group work by abstraction move rather than by file. + +Preferred moves: + +- primitive id or validated token → newtype / value object +- string or bool discriminator → enum +- tuple or clump → named struct +- repeated validation or parsing → constructor or smart parser +- repeated conversion or patch logic → dedicated helper, trait, or module +- repeated branch skeleton → extracted abstraction + +Apply the more specific move guidance in `Embedded Refactor Playbook`. + +Preserve public API compatibility only if the user asked for it. Otherwise prefer the clean shape. + +### 8. Verify and sweep + +After edits: + +- run relevant checks on the touched surface +- re-scan the same manifest +- update the worklog with eliminated rows, deferred rows, false positives, and residual hotspots + +Prefer the narrowest checks that still validate the changed surface, then widen if needed. + +## Embedded Forms + +### Worklog Skeleton + +Create one persistent worklog file per extermination run. + +```text +worklog_path: +scope_root: +inspection_mode: audit_only | audit_plus_refactor +wave_size_limit: 8 + +manifest: + +clique_plan: + +wave_checkpoints: + +rejected_clusters: + +promoted_ledger: + +residual_sweep: +``` + +Rules: + +- create this file before the first code read +- update it after every 8-file wave before reading more files +- treat it as the durable source of truth for the run +- if chat summaries are shorter, the worklog still must remain complete +- report the final worklog path in the user-facing response + +### File Manifest + +Build this before analysis starts. + +```text +scope_root: +inspection_mode: audit_only | audit_plus_refactor +wave_size_limit: 8 + +files: +- [ ] path/to/file_a.rs +- [ ] path/to/file_b.rs +- [ ] path/to/file_c.rs +``` + +Rules: + +- include every `*.rs` file in the requested subtree +- mark files as inspected only after reading them +- allow files to appear in multiple clique waves +- do not delete rows from the manifest during refactoring +- use the same manifest again for the residual sweep + +### Clique Wave Checkpoint + +Plan and checkpoint every bounded read wave with this format. + +```text +clique_id: +purpose: +why_these_files_belong_together: +wave_size: + +files: +- path/to/file_a.rs +- path/to/file_b.rs + +checkpoint: +- raw suspects: +- duplication hypotheses: +- likely rereads: +- abstraction pressure: +``` + +Rules: + +- `wave_size` must never exceed `8` +- write a checkpoint into the `/tmp` worklog after every wave before reading more files +- keep the checkpoint terse and raw; do not inflate every suspect into a formal ledger row yet +- a file may appear in multiple cliques when duplication or boundary reasoning demands it +- split oversized conceptual groups into multiple related cliques rather than inflating one wave +- name the clique by the suspected relationship, not by arbitrary adjacency + +### Rejected Cluster + +Use this for suspect clusters that were noticed during coverage but did not earn formal ledger rows. + +```text +cluster_id: +kind: +sites: +- path/to/file_a.rs :: short note +- path/to/file_b.rs :: short note + +rejection_reason: +still_watch_for: +``` + +Good rejection reasons: + +- boundary-shaped transport code +- serialization or protocol surface where primitive form is correct +- cosmetic repetition without invariant pressure +- local-only pattern with no cross-site duplication +- abstraction name still too foggy to justify extraction + +Rules: + +- keep this short +- account for every non-promoted suspect cluster somewhere +- do not use rejected clusters to hide real actionable refactors + +### Promoted Ledger + +Use one row per promoted candidate site. + +```text +| row_id | path | symbol_or_site | kind | evidence | suspected_abstraction | duplication_cluster | confidence | decision | decision_reason | +|--------|------|----------------|------|----------|-----------------------|---------------------|------------|----------|-----------------| +| S001 | crates/foo/src/bar.rs | User.id: String | string | parsed, validated, and compared in 3 places | UserId newtype | id-handling-1 | high | refactor_now | domain identifier with repeated normalization | +| B001 | crates/foo/src/baz.rs | frobnicate(..., dry_run: bool) | bool | positional flag controls strategy | ExecutionMode enum | mode-flags-1 | high | refactor_now | bool hides a mode split | +| T001 | crates/foo/src/qux.rs | (start, end, step) | tuple | repeated destructuring across module boundary | RangeSpec struct | tuple-shapes-2 | medium | defer | local only today, but cluster suggests future extraction | +``` + +Allowed `kind` values: + +- `string` +- `bool` +- `tuple` +- `parameter_clump` +- `field_bundle` +- `duplicate_logic` +- `primitive_control_encoding` + +Allowed `confidence` values: + +- `high` +- `medium` +- `low` + +Allowed `decision` values: + +- `refactor_now` +- `defer` +- `false_positive` +- `needs_broader_design` + +Requirements: + +- `evidence` must name the concrete smell, not a vague feeling +- `suspected_abstraction` must be specific +- `duplication_cluster` must group related rows when applicable +- `decision_reason` must explain why the row did or did not graduate into an edit +- do not create rows for suspects that failed promotion; summarize those as rejected clusters instead + +## Embedded Adjudication Checklist + +Run this checklist for every ledger row. + +### Semantic Pressure + +- Does this primitive carry domain semantics rather than mere transport? +- Does it have validation, normalization, parsing, or formatting rules? +- Does the code rely on an implied finite value set? +- Would naming the concept clarify the surrounding code immediately? + +### Invariant Pressure + +- Are some values invalid but still representable today? +- Are correlated values allowed to drift apart? +- Does ordering matter only because a tuple hid the field names? +- Are impossible states currently encoded with primitive combinations? + +### Boundary Pressure + +- Does the primitive cross module or crate boundaries? +- Does it appear in public APIs, trait methods, or repeated call chains? +- Is the same primitive interpretation duplicated at multiple boundaries? + +### Duplication Pressure + +- Is the same parse, validate, normalize, compare, or convert logic repeated? +- Does the same field bundle move together repeatedly? +- Is there a repeated branch skeleton with only small local differences? + +### Refactor Readiness + +- Can a local type or helper eliminate repetition immediately? +- Will the fix cascade cleanly through references if renamed mechanically? +- Is there a natural owner module for the new abstraction? +- Does the change need a broader domain redesign first? + +### Decision Mapping + +Choose exactly one: + +- `refactor_now`: the abstraction is clear and locally actionable now +- `defer`: the smell is real but the present diff would be premature +- `false_positive`: the primitive is genuinely incidental or boundary-shaped +- `needs_broader_design`: the smell is real but the right abstraction spans a larger domain cut + +## Embedded Refactor Playbook + +Prefer these moves when the ledger supports them. + +### String + +- identifiers → newtypes +- finite tags / statuses / modes → enums +- validated textual concepts → smart constructors plus opaque wrappers +- repeated path / URL / email / locale handling → dedicated domain type or parser boundary + +### Bool + +- mode switches → enums +- policy flags → policy types +- correlated bool sets → state enum or config struct with named fields +- public boolean parameters → named option type unless the meaning is truly trivial + +### Tuple + +- cross-module tuples → named structs +- repeated destructuring → named fields +- semantically rich returns → domain object instead of positional packs + +### Parameter Clumps And Bundles + +- repeated argument groups → parameter object +- repeated local field packs → extracted struct +- repeated construction/update logic → dedicated constructor or helper module + +### Duplicate Logic + +- repeated validation → single constructor / validator +- repeated conversions → `From` / `TryFrom` / dedicated conversion function +- repeated branch skeletons → extracted helper, trait, or dispatch enum +- repeated formatting → single renderer / formatter + +### Rejection Criteria + +Do not introduce an abstraction merely because a primitive exists. + +Reject or defer when: + +- the primitive is truly incidental and carries no stable semantics +- the abstraction name is still foggy +- the same concept appears only once and shows no invariant pressure +- the code is at a serialization boundary where the primitive form is the correct external shape + +## Final Response + +Always include: + +- the `/tmp` worklog path +- manifest coverage summary +- clique-wave summary +- rejected-cluster summary +- promoted-ledger summary + +If you edited code, also include: + +- abstraction batches executed +- verification summary +- residual sweep summary + +If you did not edit code, include: + +- highest-value next moves + +## Hard Failure Modes + +- do not inspect only “important” files +- do not skip the `/tmp` worklog or keep checkpoint state only in chat +- do not read more than 8 files without a persisted checkpoint +- do not silently compact a wave because you feel you “basically got it” +- do not treat one clique as if it exhausts all duplication relationships +- do not promote everything into the formal ledger just to feel exhaustive +- do not drop non-promoted suspects on the floor; summarize them as rejected clusters +- do not jump into edits before the promoted ledger exists +- do not propose generic cleanup without row-level evidence +- do not treat Clippy output as a substitute for this pass diff --git a/skills/exterminate-slop/agents/openai.yaml b/skills/exterminate-slop/agents/openai.yaml new file mode 100644 index 0000000..ea5efab --- /dev/null +++ b/skills/exterminate-slop/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Exterminate Slop" + short_description: "Exhaustive Rust slop eradication" + default_prompt: "Use $exterminate-slop to perform a decision-per-row Rust subtree pass for primitive obsession, under-abstraction, and DRY violations." diff --git a/skills/rust-bootstrap/SKILL.md b/skills/rust-bootstrap/SKILL.md new file mode 100644 index 0000000..499bf04 --- /dev/null +++ b/skills/rust-bootstrap/SKILL.md @@ -0,0 +1,53 @@ +--- +name: rust-bootstrap +description: Bootstrap or retrofit industrial-grade Rust linting by using the `rust_starter` repo as the source of truth. Use when Codex needs to set up strict Rust lint/check posture from scratch in a new repo, or ratchet an existing Rust repo toward manifest-owned lint policy without blindly overwriting valid local exceptions or stricter existing rules. +--- + +# Rust Bootstrap + +Read [AGENTS.md](/home/main/programming/projects/rust_starter/AGENTS.md) first. + +Then choose exactly one surface. + +## Fresh Bootstrap + +Use this when the target repo is blank or when you are setting up Rust linting from scratch. + +Read: + +- [docs/bootstrap-fresh.md](/home/main/programming/projects/rust_starter/docs/bootstrap-fresh.md) +- [template/fresh](/home/main/programming/projects/rust_starter/template/fresh) + +Apply the template deliberately: + +- replace placeholder members, names, and version/toolchain values +- keep repo-wide policy in root `Cargo.toml` +- keep `clippy.toml` tiny or delete it +- keep runners thin and orchestration-only + +## Retrofit + +Use this when the target repo already has Rust linting or check scripts and needs tightening. + +Read: + +- [docs/bootstrap-retrofit.md](/home/main/programming/projects/rust_starter/docs/bootstrap-retrofit.md) +- [template/fresh](/home/main/programming/projects/rust_starter/template/fresh) +- [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md) + +Retrofit is diff-aware ratchet work: + +- do not paste the fresh template wholesale +- preserve stricter local policy +- preserve justified local exceptions +- move duplicated script or CI flags into manifest-owned policy +- delete duplicate policy copies only after the manifest is authoritative + +## Guardrails + +- Re-open the repo docs when details matter; do not rely on memory. +- Use [docs/rust-linting-proposal.md](/home/main/programming/projects/rust_starter/docs/rust-linting-proposal.md) for rationale, not as a paste target. +- If a target repo already exceeds the template in some area, keep the stricter local posture. +- If a target repo intentionally diverges, adapt the pattern instead of forcing uniformity. + +This skill is intentionally thin. The repo docs are the real payload. diff --git a/skills/rust-bootstrap/agents/openai.yaml b/skills/rust-bootstrap/agents/openai.yaml new file mode 100644 index 0000000..2dbdc72 --- /dev/null +++ b/skills/rust-bootstrap/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Rust Bootstrap" + short_description: "Bootstrap or tighten Rust linting" + default_prompt: "Use rust_starter to bootstrap or retrofit industrial-grade Rust linting." diff --git a/template/fresh/Cargo.lock b/template/fresh/Cargo.lock new file mode 100644 index 0000000..791fca9 --- /dev/null +++ b/template/fresh/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "app" +version = "0.1.0" diff --git a/template/fresh/Cargo.toml b/template/fresh/Cargo.toml new file mode 100644 index 0000000..4c6b49f --- /dev/null +++ b/template/fresh/Cargo.toml @@ -0,0 +1,118 @@ +# Replace the member list, package metadata, and toolchain/MSRV values deliberately. + +[workspace] +members = ["crates/app"] +resolver = "3" + +[workspace.package] +edition = "2024" +license = "MIT" +rust-version = "1.90" +version = "0.1.0" + +[workspace.lints.rust] +elided_lifetimes_in_paths = "deny" +unexpected_cfgs = "deny" +unsafe_code = "deny" +unused_crate_dependencies = "warn" +unused_lifetimes = "deny" +unused_qualifications = "deny" +unused_results = "deny" + +[workspace.lints.rustdoc] +bare_urls = "deny" +broken_intra_doc_links = "deny" + +[workspace.lints.clippy] +all = { level = "deny", priority = -2 } +pedantic = { level = "deny", priority = -1 } +cargo = { level = "warn", priority = -3 } + +dbg_macro = "deny" +expect_used = "deny" +panic = "deny" +todo = "deny" +unimplemented = "deny" +unwrap_used = "deny" +allow_attributes_without_reason = "deny" + +cargo_common_metadata = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +multiple_crate_versions = "allow" + +items_after_statements = "allow" +many_single_char_names = "allow" +match_same_arms = "allow" +module_name_repetitions = "allow" +similar_names = "allow" +struct_field_names = "allow" +too_many_arguments = "allow" +too_many_lines = "allow" +unnested_or_patterns = "allow" + +cast_lossless = "allow" +cast_possible_truncation = "allow" +cast_possible_wrap = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" +float_cmp = "allow" +implicit_hasher = "allow" +manual_let_else = "allow" +map_unwrap_or = "allow" +uninlined_format_args = "allow" + +ignored_unit_patterns = "allow" +must_use_candidate = "allow" +needless_pass_by_value = "allow" +no_effect_underscore_binding = "allow" +redundant_closure_for_method_calls = "allow" +ref_option = "allow" +return_self_not_must_use = "allow" +trivially_copy_pass_by_ref = "allow" +unused_async = "allow" +used_underscore_binding = "allow" + +[workspace.metadata.rust-starter] +format_command = ["cargo", "fmt", "--all", "--check"] +clippy_command = [ + "cargo", + "clippy", + "--workspace", + "--all-targets", + "--all-features", + "--", + "-D", + "warnings", +] +test_command = ["cargo", "test", "--workspace", "--all-targets", "--all-features"] +doc_command = ["cargo", "doc", "--workspace", "--all-features", "--no-deps"] +canonicalize_commands = [ + [ + "cargo", + "fix", + "--workspace", + "--all-targets", + "--all-features", + "--allow-dirty", + "--allow-staged", + "--allow-no-vcs", + ], + [ + "cargo", + "clippy", + "--fix", + "--workspace", + "--all-targets", + "--all-features", + "--allow-dirty", + "--allow-staged", + "--allow-no-vcs", + ], + ["cargo", "fmt", "--all"], +] + +[workspace.metadata.rust-starter.source_files] +max_lines = 2500 +include = ["*.rs", "**/*.rs"] +exclude = [] diff --git a/template/fresh/check.py b/template/fresh/check.py new file mode 100644 index 0000000..424497e --- /dev/null +++ b/template/fresh/check.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +import subprocess +import tomllib +from dataclasses import dataclass +from pathlib import Path +from pathlib import PurePosixPath + + +ROOT = Path(__file__).resolve().parent +WORKSPACE_MANIFEST = ROOT / "Cargo.toml" +DEFAULT_MAX_SOURCE_FILE_LINES = 2500 +DEFAULT_SOURCE_FILE_INCLUDE = ("*.rs", "**/*.rs") +IGNORED_SOURCE_DIRS = frozenset( + {".direnv", ".git", ".hg", ".jj", ".svn", "__pycache__", "node_modules", "target", "vendor"} +) +Command = tuple[str, ...] +CommandSequence = tuple[Command, ...] + + +@dataclass(frozen=True, slots=True) +class SourceFilePolicy: + max_lines: int + include: tuple[str, ...] + exclude: tuple[str, ...] + + +def load_workspace_metadata() -> dict[str, object]: + workspace = tomllib.loads(WORKSPACE_MANIFEST.read_text(encoding="utf-8")) + return workspace["workspace"]["metadata"]["rust-starter"] + + +def load_command(value: object, *, key_path: str) -> Command: + if not isinstance(value, list) or not value or not all(isinstance(part, str) and part for part in value): + raise SystemExit(f"[check] invalid {key_path}: expected a non-empty string list") + return tuple(value) + + +def load_command_sequence(value: object, *, key_path: str) -> CommandSequence: + if not isinstance(value, list) or not value: + raise SystemExit(f"[check] invalid {key_path}: expected a non-empty list of commands") + + commands: list[Command] = [] + for index, command in enumerate(value, start=1): + commands.append(load_command(command, key_path=f"{key_path}[{index}]")) + return tuple(commands) + + +def load_commands(metadata: dict[str, object]) -> dict[str, Command | CommandSequence]: + commands: dict[str, Command | CommandSequence] = { + "format_command": load_command( + metadata.get("format_command"), + key_path="workspace.metadata.rust-starter.format_command", + ), + "clippy_command": load_command( + metadata.get("clippy_command"), + key_path="workspace.metadata.rust-starter.clippy_command", + ), + "test_command": load_command( + metadata.get("test_command"), + key_path="workspace.metadata.rust-starter.test_command", + ), + "canonicalize_commands": load_command_sequence( + metadata.get("canonicalize_commands"), + key_path="workspace.metadata.rust-starter.canonicalize_commands", + ), + } + + raw_doc_command = metadata.get("doc_command") + if raw_doc_command is not None: + commands["doc_command"] = load_command( + raw_doc_command, + key_path="workspace.metadata.rust-starter.doc_command", + ) + return commands + + +def load_patterns( + value: object, + *, + default: tuple[str, ...], + key_path: str, + allow_empty: bool, +) -> tuple[str, ...]: + if value is None: + return default + if not isinstance(value, list) or not all(isinstance(pattern, str) and pattern for pattern in value): + raise SystemExit(f"[check] invalid {key_path}: expected a string list") + if not allow_empty and not value: + raise SystemExit(f"[check] invalid {key_path}: expected at least one pattern") + return tuple(value) + + +def load_source_file_policy(metadata: dict[str, object]) -> SourceFilePolicy: + raw_policy = metadata.get("source_files") + if raw_policy is None: + return SourceFilePolicy(DEFAULT_MAX_SOURCE_FILE_LINES, DEFAULT_SOURCE_FILE_INCLUDE, ()) + if not isinstance(raw_policy, dict): + raise SystemExit("[check] invalid workspace.metadata.rust-starter.source_files: expected a table") + + max_lines = raw_policy.get("max_lines", DEFAULT_MAX_SOURCE_FILE_LINES) + if not isinstance(max_lines, int) or max_lines <= 0: + raise SystemExit( + "[check] invalid workspace.metadata.rust-starter.source_files.max_lines: expected a positive integer" + ) + + include = load_patterns( + raw_policy.get("include"), + default=DEFAULT_SOURCE_FILE_INCLUDE, + key_path="workspace.metadata.rust-starter.source_files.include", + allow_empty=False, + ) + exclude = load_patterns( + raw_policy.get("exclude"), + default=(), + key_path="workspace.metadata.rust-starter.source_files.exclude", + allow_empty=True, + ) + return SourceFilePolicy(max_lines, include, exclude) + + +def run(name: str, argv: Command) -> None: + print(f"[check] {name}: {' '.join(argv)}", flush=True) + proc = subprocess.run(argv, cwd=ROOT) + if proc.returncode != 0: + raise SystemExit(proc.returncode) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Thin Rust starter check runner") + parser.add_argument( + "mode", + nargs="?", + choices=("check", "verify", "deep", "fix", "canon"), + default="check", + help=( + "Run canonicalization plus the fast gate, run a non-mutating verification gate, " + "include docs for the deep gate, or run only canonicalization." + ), + ) + return parser.parse_args() + + +def run_command_sequence(name: str, commands: CommandSequence) -> None: + for index, command in enumerate(commands, start=1): + run(f"{name}.{index}", command) + + +def matches_pattern(path: PurePosixPath, pattern: str) -> bool: + if path.match(pattern): + return True + prefix = "**/" + return pattern.startswith(prefix) and path.match(pattern.removeprefix(prefix)) + + +def iter_source_files(policy: SourceFilePolicy) -> list[Path]: + paths: list[Path] = [] + for current_root, dirnames, filenames in os.walk(ROOT): + dirnames[:] = sorted(name for name in dirnames if name not in IGNORED_SOURCE_DIRS) + current = Path(current_root) + for filename in filenames: + path = current / filename + relative_path = PurePosixPath(path.relative_to(ROOT).as_posix()) + if not any(matches_pattern(relative_path, pattern) for pattern in policy.include): + continue + if any(matches_pattern(relative_path, pattern) for pattern in policy.exclude): + continue + paths.append(path) + return sorted(paths) + + +def line_count(path: Path) -> int: + return len(path.read_text(encoding="utf-8").splitlines()) + + +def enforce_source_file_policy(policy: SourceFilePolicy) -> None: + paths = iter_source_files(policy) + print(f"[check] source-files: max {policy.max_lines} lines", flush=True) + violations: list[tuple[str, int]] = [] + for path in paths: + lines = line_count(path) + if lines > policy.max_lines: + violations.append((path.relative_to(ROOT).as_posix(), lines)) + if not violations: + return + + print( + f"[check] source-files: {len(violations)} file(s) exceed the configured limit", + flush=True, + ) + for relative_path, lines in violations: + print(f"[check] source-files: {relative_path}: {lines} lines", flush=True) + raise SystemExit(1) + + +def main() -> None: + metadata = load_workspace_metadata() + commands = load_commands(metadata) + source_file_policy = load_source_file_policy(metadata) + args = parse_args() + + if args.mode in {"fix", "canon"}: + run_command_sequence("canonicalize", commands["canonicalize_commands"]) + return + + enforce_source_file_policy(source_file_policy) + if args.mode != "verify": + run_command_sequence("canonicalize", commands["canonicalize_commands"]) + + run("fmt", commands["format_command"]) + run("clippy", commands["clippy_command"]) + run("test", commands["test_command"]) + + if args.mode == "deep" and "doc_command" in commands: + run("doc", commands["doc_command"]) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + raise SystemExit(130) diff --git a/template/fresh/clippy.toml b/template/fresh/clippy.toml new file mode 100644 index 0000000..b3be953 --- /dev/null +++ b/template/fresh/clippy.toml @@ -0,0 +1,5 @@ +# Keep this file tiny. Do not move the repo-wide allow/deny architecture here. + +allow-expect-in-tests = true +allow-unwrap-in-tests = true +allow-panic-in-tests = true diff --git a/template/fresh/crates/app/Cargo.toml b/template/fresh/crates/app/Cargo.toml new file mode 100644 index 0000000..ce1808a --- /dev/null +++ b/template/fresh/crates/app/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "app" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true diff --git a/template/fresh/crates/app/src/main.rs b/template/fresh/crates/app/src/main.rs new file mode 100644 index 0000000..7527576 --- /dev/null +++ b/template/fresh/crates/app/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("hello"); +} diff --git a/template/fresh/rust-toolchain.toml b/template/fresh/rust-toolchain.toml new file mode 100644 index 0000000..66f329f --- /dev/null +++ b/template/fresh/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.90.0" +profile = "minimal" +components = ["clippy", "rustfmt"] -- cgit v1.2.3