@thedecipherist/claude-packs 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +275 -18
  3. package/bin/claude-packs.mjs +18 -10
  4. package/configurations/base/config.json +1 -1
  5. package/configurations/cpp/config.json +2 -1
  6. package/configurations/full-stack/config.json +2 -1
  7. package/configurations/go/config.json +3 -1
  8. package/configurations/node/config.json +3 -1
  9. package/configurations/python/config.json +3 -1
  10. package/configurations/rust/config.json +106 -0
  11. package/configurations/synth-creator/config.json +1 -1
  12. package/global/hooks/lib/shell-writes.mjs +2 -293
  13. package/lib/build-queue.mjs +22 -0
  14. package/lib/claude-md.mjs +4 -2
  15. package/lib/config-cli.mjs +1 -1
  16. package/lib/coverage.mjs +10 -2
  17. package/lib/doctor.mjs +14 -3
  18. package/lib/fix-plan.mjs +2 -1
  19. package/lib/found-folders.mjs +25 -0
  20. package/lib/global.mjs +3 -3
  21. package/lib/hook-proof.mjs +24 -7
  22. package/lib/kit-command.mjs +14 -0
  23. package/lib/layout.mjs +27 -3
  24. package/lib/logs.mjs +45 -5
  25. package/lib/new.mjs +1 -1
  26. package/lib/packs.mjs +4 -2
  27. package/lib/rule-tests.mjs +42 -0
  28. package/lib/scan-detect.mjs +41 -5
  29. package/lib/scan.mjs +5 -2
  30. package/lib/suggest.mjs +16 -3
  31. package/lib/sync.mjs +5 -2
  32. package/lib/workorder.mjs +26 -0
  33. package/package.json +1 -1
  34. package/packs/audio-core/hooks/param-id-lock.mjs +59 -16
  35. package/packs/audio-core/pack.json +5 -0
  36. package/packs/audio-core/rules/a-control-is-proved-through-its-input.md +26 -0
  37. package/packs/audio-core/rules/cpu-worst-case-per-block.md +3 -1
  38. package/packs/audio-core/rules/house-control-behaviours.md +1 -1
  39. package/packs/audio-core/rules/scoped-no-denormals.md +1 -1
  40. package/packs/audio-core/rules/straight-on-and-aligned.md +1 -1
  41. package/packs/audio-core/rules/ui-matches-reference-look.md +2 -0
  42. package/packs/audio-core/skills/analyse-reference-look/SKILL.md +25 -11
  43. package/packs/audio-core/skills/prove-control/SKILL.md +2 -0
  44. package/packs/base/checks/one-walk-per-template.json +1 -1
  45. package/packs/base/hooks/code-shape.mjs +10 -6
  46. package/packs/base/hooks/dispatch-once.mjs +58 -21
  47. package/packs/base/hooks/folder-law.mjs +3 -0
  48. package/packs/base/hooks/git-guard.mjs +22 -109
  49. package/packs/base/hooks/lesson-checks.mjs +9 -1
  50. package/packs/base/hooks/lib/code-shapes.mjs +11 -6
  51. package/packs/base/hooks/lib/crash-policy.mjs +47 -0
  52. package/packs/base/hooks/lib/dup-index.mjs +6 -1
  53. package/packs/base/hooks/lib/dup.mjs +2 -2
  54. package/packs/base/hooks/lib/event-log.mjs +4 -3
  55. package/packs/base/hooks/lib/git-adds.mjs +2 -1
  56. package/packs/base/hooks/lib/git-auth.mjs +44 -0
  57. package/packs/base/hooks/lib/git-guard-selftest.mjs +93 -0
  58. package/packs/base/hooks/lib/languages.mjs +5 -2
  59. package/packs/base/hooks/lib/layout.mjs +6 -2
  60. package/packs/base/hooks/lib/making-check-selftest.mjs +3 -0
  61. package/packs/base/hooks/lib/near-copies.mjs +1 -1
  62. package/packs/base/hooks/lib/program-read.mjs +6 -3
  63. package/packs/base/hooks/lib/rust-scan.mjs +139 -0
  64. package/packs/base/hooks/lib/shape-lists.mjs +31 -0
  65. package/packs/base/hooks/lib/shape-prose.mjs +78 -0
  66. package/packs/base/hooks/lib/shape.mjs +18 -39
  67. package/packs/base/hooks/lib/turn.mjs +6 -1
  68. package/packs/base/hooks/lib/unread-writes.mjs +24 -0
  69. package/packs/base/hooks/making-check.mjs +12 -7
  70. package/packs/base/hooks/run-hooks.mjs +15 -7
  71. package/packs/base/hooks/shape-law.mjs +11 -4
  72. package/packs/base/hooks/suite-once.mjs +22 -6
  73. package/packs/base/rules/a-control-is-seen-to-fire.md +1 -1
  74. package/packs/base/rules/behaviour-lives-with-its-kind.md +1 -1
  75. package/packs/base/rules/bug-fix-starts-red.md +1 -1
  76. package/packs/base/rules/field-map-over-if-runs.md +1 -1
  77. package/packs/base/rules/instances-by-index-identities-by-id.md +2 -2
  78. package/packs/base/rules/lookup-over-if-chains.md +1 -1
  79. package/packs/base/rules/plan-and-logs.md +1 -1
  80. package/packs/base/rules/refusals-are-judged.md +1 -0
  81. package/packs/base/rules/test-the-template-once.md +2 -1
  82. package/packs/base/rules/unattended-runs.md +15 -0
  83. package/packs/base/skills/new-project/SKILL.md +23 -0
  84. package/packs/base/skills/write-a-workorder/SKILL.md +41 -0
  85. package/packs/cpp/checks/cpp-find-with-the-library.json +2 -2
  86. package/packs/go/pack.json +5 -0
  87. package/packs/hook-proof/rules/a-refusal-is-exit-2.md +2 -2
  88. package/packs/instrument-shell/rules/render-the-extremes.md +2 -2
  89. package/packs/midi/checks/position-not-integrated.json +5 -1
  90. package/packs/postgres/checks/pg-bind-values.json +33 -0
  91. package/packs/postgres/checks/pg-concurrently-in-txn.json +18 -0
  92. package/packs/postgres/checks/pg-fk-not-valid.json +18 -0
  93. package/packs/postgres/checks/pg-index-concurrently.json +18 -0
  94. package/packs/postgres/checks/pg-no-float-money.json +19 -0
  95. package/packs/postgres/checks/pg-no-money.json +18 -0
  96. package/packs/postgres/checks/pg-no-required-column.json +18 -0
  97. package/packs/postgres/checks/pg-no-volatile-default.json +18 -0
  98. package/packs/postgres/checks/pg-not-exists.json +18 -0
  99. package/packs/postgres/checks/pg-timestamptz.json +18 -0
  100. package/packs/postgres/hooks/db-wipe-guard.mjs +95 -0
  101. package/packs/postgres/pack.json +48 -0
  102. package/packs/postgres/rules/postgres-migrations-never-stop-traffic.md +28 -0
  103. package/packs/postgres/rules/postgres-one-pool-per-process.md +21 -0
  104. package/packs/postgres/rules/postgres-queries-are-null-safe-and-bounded.md +26 -0
  105. package/packs/postgres/rules/postgres-types-say-what-they-hold.md +22 -0
  106. package/packs/postgres/rules/postgres-values-are-bound.md +130 -0
  107. package/packs/postgres/skills/postgres-safe-migration/SKILL.md +45 -0
  108. package/packs/reverse-engineering/rules/addresses-name-their-space.md +1 -0
  109. package/packs/reverse-engineering/rules/reproduction-or-original.md +1 -1
  110. package/packs/reverse-engineering/skills/gather-sources/SKILL.md +1 -0
  111. package/packs/rust/checks/rust-allow-needs-reason.json +16 -0
  112. package/packs/rust/checks/rust-blocking-in-async.json +32 -0
  113. package/packs/rust/checks/rust-if-chain-is-a-match.json +32 -0
  114. package/packs/rust/checks/rust-static-mut.json +32 -0
  115. package/packs/rust/checks/rust-string-as-error.json +34 -0
  116. package/packs/rust/checks/rust-stub-left-in.json +32 -0
  117. package/packs/rust/checks/rust-unsafe-needs-safety.json +31 -0
  118. package/packs/rust/checks/rust-unwrap-in-library.json +34 -0
  119. package/packs/rust/checks/rust-wildcard-dependency.json +16 -0
  120. package/packs/rust/clippy.toml +5 -0
  121. package/packs/rust/pack.json +27 -0
  122. package/packs/rust/rules/rust-async-never-blocks.md +20 -0
  123. package/packs/rust/rules/rust-borrow-before-you-clone.md +19 -0
  124. package/packs/rust/rules/rust-errors-are-returned.md +22 -0
  125. package/packs/rust/rules/rust-lints-are-the-gate.md +23 -0
  126. package/packs/rust/rules/rust-repeats-become-templates.md +28 -0
  127. package/packs/rust/rules/rust-unsafe-is-proved.md +21 -0
  128. package/packs/rust/skills/rust-extract-template/SKILL.md +50 -0
  129. package/packs/rust/skills/rust-port/SKILL.md +22 -0
  130. package/packs/rust/skills/rust-review/SKILL.md +24 -0
  131. package/packs/rust/tools/rust-lints.toml +19 -0
  132. package/packs/sound-design/skills/bounce-a-plugin/SKILL.md +40 -0
  133. package/packs/sound-design/tools/vst3host/build.sh +73 -0
  134. package/packs/sound-design/tools/vst3host/host_events.h +101 -0
  135. package/packs/sound-design/tools/vst3host/options.h +172 -0
  136. package/packs/sound-design/tools/vst3host/render.cpp +245 -0
  137. package/packs/sound-design/tools/vst3host/session.h +241 -0
  138. package/packs/synth-creator/hooks/build-guard.mjs +7 -7
  139. package/packs/synth-creator/hooks/protect-vendor.mjs +2 -1
  140. package/packs/synth-creator/hooks/stage-guard.mjs +1 -1
  141. package/packs/synth-creator/rules/timing-from-synthcore.md +1 -1
  142. package/packs/synth-creator/skills/build-through-the-queue/SKILL.md +4 -2
  143. package/packs/synth-creator/tools/buildq.mjs +74 -0
  144. package/prompts/workorder/PROMPT.md +53 -0
  145. package/prompts/workorder/run.sh +36 -0
  146. package/prompts/workorder/workorder.md +59 -0
  147. package/global/hooks/lib/program-read.mjs +0 -196
  148. package/global/hooks/lib/program-writes.mjs +0 -177
@@ -0,0 +1,31 @@
1
+ {
2
+ "id": "rust-unsafe-needs-safety",
3
+ "why": "An unsafe block with no stated reason cannot be reviewed.",
4
+ "rule": ".claude/rules/rust-unsafe-is-proved.md",
5
+ "globs": [
6
+ "**/*.rs"
7
+ ],
8
+ "exclude": [
9
+ "**/tests/**",
10
+ "**/test/**",
11
+ "**/benches/**",
12
+ "**/examples/**",
13
+ "**/fixtures/**",
14
+ "**/test_data/**",
15
+ "**/testdata/**",
16
+ "**/fuzz/**",
17
+ "**/xtask/**",
18
+ "**/target/**",
19
+ "**/vendor/**",
20
+ "**/build.rs",
21
+ "**/tests.rs",
22
+ "**/test.rs",
23
+ "**/*_test.rs",
24
+ "**/*_tests.rs",
25
+ "**/test_*.rs"
26
+ ],
27
+ "pattern": "^(?<!(?:^|\\n)[ \\t]*//[^\\n]*[Ss][Aa][Ff][Ee][Tt][Yy][^\\n]*\\n(?:[ \\t]*//[^\\n]*\\n)*)(?![ \\t]*//)(?![^\\n]*//[^\\n]*SAFETY)[^\\n]*\\bunsafe\\s*\\{",
28
+ "flags": "m",
29
+ "bad": "fn f(p: *const u8) -> u8 {\n unsafe { *p }\n}",
30
+ "good": "fn f(p: *const u8) -> u8 {\n // SAFETY: the caller passes a live pointer, checked in new().\n unsafe { *p }\n}"
31
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "id": "rust-unwrap-in-library",
3
+ "why": "A failure the caller could handle becomes a panic with no reason. Return it with `?`, or say why it cannot happen with `.expect(\"...\")`.",
4
+ "rule": ".claude/rules/rust-errors-are-returned.md",
5
+ "globs": [
6
+ "**/*.rs"
7
+ ],
8
+ "exclude": [
9
+ "**/tests/**",
10
+ "**/test/**",
11
+ "**/benches/**",
12
+ "**/examples/**",
13
+ "**/fixtures/**",
14
+ "**/test_data/**",
15
+ "**/testdata/**",
16
+ "**/fuzz/**",
17
+ "**/xtask/**",
18
+ "**/target/**",
19
+ "**/vendor/**",
20
+ "**/build.rs",
21
+ "**/tests.rs",
22
+ "**/test.rs",
23
+ "**/*_test.rs",
24
+ "**/*_tests.rs",
25
+ "**/test_*.rs",
26
+ "**/main.rs",
27
+ "**/bin/**"
28
+ ],
29
+ "language": "rust",
30
+ "notInTests": true,
31
+ "pattern": "(?<!\\.lock\\(\\)|\\.read\\(\\)|\\.write\\(\\))\\.unwrap\\(\\)",
32
+ "bad": "let n = text.parse::<u32>().unwrap();",
33
+ "good": "let n = text.parse::<u32>()?;"
34
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "rust-wildcard-dependency",
3
+ "why": "A * version takes whatever is newest, and crates.io refuses it.",
4
+ "rule": ".claude/rules/rust-lints-are-the-gate.md",
5
+ "globs": [
6
+ "**/Cargo.toml"
7
+ ],
8
+ "exclude": [
9
+ "**/target/**",
10
+ "**/vendor/**"
11
+ ],
12
+ "pattern": "(?:^[ \\t]*[\\w-]+\\s*=\\s*|\\bversion\\s*=\\s*)\"\\*\"",
13
+ "flags": "m",
14
+ "bad": "serde = \"*\"",
15
+ "good": "serde = \"1.0\""
16
+ }
@@ -0,0 +1,5 @@
1
+ # Unit tests live in the file they test: the lints that hold library code leave them alone.
2
+ allow-unwrap-in-tests = true
3
+ allow-expect-in-tests = true
4
+ allow-dbg-in-tests = true
5
+ allow-panic-in-tests = true
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "rust",
3
+ "version": "1.0.0",
4
+ "description": "Rust: errors returned, unsafe proved, async never blocks, clippy as the gate. What clippy reads better stays with clippy.",
5
+ "type": "stock",
6
+ "author": "claude-packs",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/TheDecipherist/claude-packs",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/TheDecipherist/claude-packs.git",
12
+ "directory": "packs/rust"
13
+ },
14
+ "keywords": [
15
+ "rust",
16
+ "cargo",
17
+ "clippy"
18
+ ],
19
+ "engines": {
20
+ "claude-packs": ">=1.1.0"
21
+ },
22
+ "concepts": {
23
+ "rust": [
24
+ "**/*.rs"
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,20 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ ---
5
+
6
+ # An async function never blocks its thread
7
+
8
+ <constraints>
9
+ Always do:
10
+ - Hand blocking work to `spawn_blocking`, or use the runtime's own `fs`, `process` and `time`.
11
+ - Drop a `std::sync` guard before an `.await`. Use the runtime's own mutex when the lock must span one.
12
+ - Give every spawned task a way to stop, and a place its error lands.
13
+
14
+ Never do:
15
+ - Call `std::thread::sleep`, `std::fs` or `std::process::Command` in an `async fn`.
16
+ - Hold a lock across `.await`: clippy's `await_holding_lock` reads this, and the gate holds it.
17
+ </constraints>
18
+
19
+ ## Check
20
+ `rust-blocking-in-async`.
@@ -0,0 +1,19 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ ---
5
+
6
+ # Borrow first: a clone is a decision, never a way past the borrow checker
7
+
8
+ <constraints>
9
+ Always do:
10
+ - Take `&str`, `&[T]` and `&Path` in a parameter, and own only what is kept.
11
+ - When the borrow checker refuses, change who owns the value, or shorten the borrow.
12
+ - Use `impl Trait` or a generic before `Box<dyn Trait>`, unless the types differ at run time.
13
+ - Walk with iterators. Let lifetimes be elided where the compiler allows it.
14
+ - Give a value its own type (a newtype, an enum) in place of a bare `String` or a `bool` flag.
15
+
16
+ Never do:
17
+ - Add `.clone()`, `Rc` or `RefCell` only to make an error go away.
18
+ - Write a function again in another crate: `template-law` reads Rust. The `rust-extract-template` skill has the forms.
19
+ </constraints>
@@ -0,0 +1,22 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ ---
5
+
6
+ # An error is returned; a panic states why it cannot happen
7
+
8
+ <constraints>
9
+ Always do:
10
+ - Return `Result` from anything that can fail, and pass it up with `?`.
11
+ - Give a library its own error type, by hand or with `thiserror`, so a caller can match on it.
12
+ - In an application, use `anyhow` with `.context(...)` saying what was being attempted.
13
+ - Write `.expect("why this cannot fail")` for a true invariant. The message is the proof.
14
+
15
+ Never do:
16
+ - `.unwrap()` in library code. Tests, `main.rs` and `src/bin/` may.
17
+ - Return `Result<T, String>` or `Box<dyn Error>` from a public function.
18
+ - Drop a `Result` with `let _ =` on a state change.
19
+ </constraints>
20
+
21
+ ## Check
22
+ `rust-unwrap-in-library`, `rust-string-as-error`.
@@ -0,0 +1,23 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ ---
5
+
6
+ # `cargo clippy -D warnings` and `cargo fmt --check` are the gate
7
+
8
+ Clippy reads types, so what it can judge stays with it. The lesson checks stop only what is cheaper to stop at the write.
9
+
10
+ <constraints>
11
+ Always do:
12
+ - Take the lint set in `.claude/tools/rust-lints.toml` into `Cargo.toml`, and keep the installed `clippy.toml` at the root.
13
+ - Run `cargo fmt --check`, then `cargo clippy --all-targets --all-features -- -D warnings`, before landing.
14
+ - Turn a lint off with `#[expect(lint, reason = "...")]`, on the smallest item. `reason` needs Rust 1.81; on an older toolchain, disable `checks/rust-allow-needs-reason.json` in `layout.json`.
15
+ - Commit `Cargo.lock` for an application.
16
+
17
+ Never do:
18
+ - Leave `todo!()` or `dbg!()` in code that lands. `unimplemented!()` is a decision, and may stay.
19
+ - Depend on a `*` version.
20
+ </constraints>
21
+
22
+ ## Check
23
+ `rust-allow-needs-reason`, `rust-stub-left-in`, `rust-wildcard-dependency`.
@@ -0,0 +1,28 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ ---
5
+
6
+ # Anything repeated in Rust becomes one template: pick the form that fits the repeat
7
+
8
+ <constraints>
9
+ Always do:
10
+ - Match the repeat to its form. The `rust-extract-template` skill has each with an example:
11
+ - A block with other names: one function, with what differs as parameters.
12
+ - The same code over several types: a generic with a trait bound.
13
+ - The same method on many types: a trait with a default method.
14
+ - The same `impl` for many types, where a generic cannot do it: one `macro_rules!`.
15
+ - One call repeated with other arguments: a `const` table and one loop.
16
+ - The same code in two crates: the crate both depend on.
17
+ - Give an enum its own methods for what varies by variant: `state.label()`, in the enum's impl. A table of values on an enum belongs with the enum.
18
+ - Match every variant by name, so a new variant is a compile error in each place that must decide. `clippy::wildcard_enum_match_arm` holds this.
19
+ - Use `#[derive(...)]` and `thiserror`'s `#[from]` in place of hand-written impls.
20
+
21
+ Never do:
22
+ - Write `_ =>` on your own enum: it hides the next variant from the compiler.
23
+ - Write a macro where a function or a generic does the job, or a trait with one implementor.
24
+ - Treat a trait method as a copy: `from_str` on two types is the trait doing its job.
25
+ </constraints>
26
+
27
+ ## Check
28
+ `template-law` refuses a copy, a near copy, a table written as calls and a function made again. `dispatch-once` refuses a second lookup table on one enum outside the enum's own file. `once-only` refuses a copy as written. `rust-if-chain-is-a-match` refuses three `==` branches on one value.
@@ -0,0 +1,21 @@
1
+ ---
2
+ paths:
3
+ - "**/*.rs"
4
+ ---
5
+
6
+ # Every unsafe block says why it is sound
7
+
8
+ <constraints>
9
+ Always do:
10
+ - Put a `// SAFETY:` comment directly above each `unsafe` block, naming the invariant and what upholds it.
11
+ - Keep a block to one unsafe operation, and wrap it in a safe function that checks what it needs.
12
+ - Give every `unsafe fn` a `# Safety` section in its docs.
13
+ - Run `cargo miri test` on code with `unsafe`, where it can run.
14
+
15
+ Never do:
16
+ - Use `static mut`: use an atomic, `OnceLock` or a `Mutex`.
17
+ - Use `unsafe` for speed with no measurement showing the safe form is too slow.
18
+ </constraints>
19
+
20
+ ## Check
21
+ `rust-unsafe-needs-safety`, `rust-static-mut`.
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: rust-extract-template
3
+ description: Use when template-law refuses Rust code, when scan lists repeated Rust, or when copying a Rust function, impl or module to change it.
4
+ ---
5
+
6
+ # Extract a Rust repeat into one template (one job: pick the Rust form, and move every copy to it)
7
+
8
+ Follow `make-it-a-template`. This skill is its Rust forms.
9
+
10
+ <constraints>
11
+ Always do:
12
+ 1. A repeated block: one function, with what differs as parameters. A difference in behaviour is a closure parameter.
13
+ 2. The same code over several types: a generic with a trait bound.
14
+ ```rust
15
+ fn total<T: Priced>(items: &[T]) -> u64 {
16
+ items.iter().map(Priced::price).sum()
17
+ }
18
+ ```
19
+ 3. The same method on many types: a trait with a default method.
20
+ 4. The same `impl` written for many types: one `macro_rules!`, and only when a generic cannot do it.
21
+ ```rust
22
+ macro_rules! impl_id { ($($t:ident),*) => { $(impl Id for $t { fn raw(&self) -> u64 { self.0 } })* } }
23
+ impl_id!(UserId, OrderId);
24
+ ```
25
+ 5. One call with other arguments: a `const` table and one loop.
26
+ ```rust
27
+ const ROUTES: &[(&str, Handler)] = &[("/users", users), ("/orders", orders)];
28
+ for (path, handler) in ROUTES {
29
+ app.route(path, *handler);
30
+ }
31
+ ```
32
+ 6. A table of values on an enum: a method on the enum, in its own impl, with every variant named. `dispatch-once` refuses a second table outside it.
33
+ ```rust
34
+ impl State {
35
+ pub fn colour(&self) -> u32 {
36
+ match self {
37
+ State::Open => 1,
38
+ State::Closed => 4,
39
+ }
40
+ }
41
+ }
42
+ ```
43
+ 7. Fields that travel together: a struct, with `#[derive(...)]` in place of hand-written impls. A run of `impl From<X> for MyError`: `thiserror`'s `#[from]`.
44
+ 8. The same code in two crates: move it to the crate both depend on.
45
+ 9. Run what the change reaches: `cargo test -p <crate>`. Then search for the old names: none is left.
46
+
47
+ Never do:
48
+ - Write a macro where a function or a generic does the job.
49
+ - Add a trait with one implementor.
50
+ </constraints>
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: rust-port
3
+ description: Use when porting code to Rust from another language, or rewriting a module in Rust.
4
+ ---
5
+
6
+ # Port to Rust (one job: the same behaviour, in Rust's own shapes)
7
+
8
+ <constraints>
9
+ Always do:
10
+ 1. Pin the behaviour first: tests or recorded input and output from the old code, kept as the proof.
11
+ 2. Write down who owns each piece of state. A shared mutable object becomes one owner that lends, or a message to the task that owns it.
12
+ 3. Map each failure the old code can have to a variant of an error type. An exception becomes a `Result`; a null becomes an `Option`.
13
+ 4. Map an inheritance tree to an enum when the set of kinds is closed, and to a trait when it is open.
14
+ 5. Port one module at a time behind the same interface, and run the pinned tests after each.
15
+ 6. Keep a behaviour the old code has even if it looks wrong, and list it. Change it in its own step.
16
+ 7. Compare speed only after it is correct, on a release build.
17
+
18
+ Never do:
19
+ - Translate line by line: a class with getters and shared pointers everywhere is the old language in Rust's syntax.
20
+ - Reach for `Rc<RefCell<T>>` or `unsafe` to keep the old shape.
21
+ - Change behaviour and language in the same step.
22
+ </constraints>
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: rust-review
3
+ description: Use when reviewing Rust code, a Rust diff or a crate before it lands, or when asked whether Rust code is sound or idiomatic.
4
+ ---
5
+
6
+ # Review Rust (one job: find what the compiler and clippy cannot)
7
+
8
+ Run the gate first, so the review spends nothing on what tools already say: `cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test`.
9
+
10
+ <constraints>
11
+ Always do:
12
+ 1. Ownership: is each `.clone()`, `Rc`, `Arc` and `RefCell` needed, or was it a way past a borrow error? Could the caller keep the value and lend it?
13
+ 2. Errors: can a caller tell the failures apart? Is every `.expect` message a true invariant? Does a `?` lose what was being attempted?
14
+ 3. `unsafe`: does each `SAFETY` comment name a real invariant, and does the code around it uphold that? Read the callers. Check aliasing, alignment, initialization, and that a panic in the block leaves nothing half done.
15
+ 4. Concurrency: what happens when a future is dropped at each `.await`? Is state left half written? Is a channel unbounded? Is `Ordering::Relaxed` proved enough?
16
+ 5. Public API: would a new field or variant break a caller (`#[non_exhaustive]`)? Is an internal type exposed? Are `Send` and `Sync` what the caller needs?
17
+ 6. Numbers and text: an `as` cast that truncates, an overflow in release builds, a byte index into a `&str`.
18
+ 7. Tests: does a test fail without the change? Is an error path covered, not only the happy one?
19
+ 8. Report each finding with the file, the symbol and the input that breaks it. A finding you cannot give an input for is a question.
20
+
21
+ Never do:
22
+ - Report what clippy or `rustfmt` already says.
23
+ - Ask for a lifetime, a trait or a generic the code does not need.
24
+ </constraints>
@@ -0,0 +1,19 @@
1
+ # The gate's lint set: paste it into Cargo.toml. In a workspace, write [workspace.lints.rust] and [workspace.lints.clippy]
2
+ # at the root, and `[lints] workspace = true` in each crate. An application can drop unwrap_used from main.rs with an
3
+ # `#[expect(clippy::unwrap_used, reason = "...")]`. `reason` needs Rust 1.81.
4
+ [lints.rust]
5
+ unsafe_op_in_unsafe_fn = "deny"
6
+
7
+ [lints.clippy]
8
+ all = { level = "deny", priority = -1 }
9
+ unwrap_used = "deny"
10
+ todo = "deny"
11
+ dbg_macro = "deny"
12
+ allow_attributes_without_reason = "deny"
13
+ undocumented_unsafe_blocks = "deny"
14
+ multiple_unsafe_ops_per_block = "deny"
15
+ panic_in_result_fn = "deny"
16
+ redundant_clone = "deny"
17
+ clone_on_ref_ptr = "deny"
18
+ fn_params_excessive_bools = "deny"
19
+ wildcard_enum_match_arm = "deny"
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: bounce-a-plugin
3
+ description: Use when audio has to be rendered from a VST3 with no DAW, when comparing an engine with another plugin or an emulator, when fitting a law against a plugin, or when a plugin will not load in a JUCE host or plays silence.
4
+ ---
5
+
6
+ # Bounce a plugin (one job: a known input through a plugin, and the audio out, repeatable)
7
+
8
+ `.claude/tools/vst3host/` is an offline VST3 renderer: no DAW, no audio device, no window, faster than real time. It does not link JUCE, on purpose: a JUCE host cannot load every JUCE plugin.
9
+
10
+ ```
11
+ bash .claude/tools/vst3host/build.sh # once, after the project's first configure; macOS and Linux
12
+ R=out/vst3host/render
13
+ $R --vst3 <plugin> --list # every parameter, with its id
14
+ $R --vst3 <plugin> --out a.wav --note 60 --vel 100 --hold 1.5 --tail 0.6
15
+ $R --vst3 <plugin> --param 12=0.75 --sweep 38:9 --dir out/ # one note per value of 38, 12 frozen
16
+ $R --vst3 <plugin> --program 5 --bank 0:1 --out a.wav # the same factory patch a machine was recorded on
17
+ $R --vst3 <plugin> --sysex patch.syx --dump-params # send a patch, and read whether it was taken
18
+ ```
19
+
20
+ The WAV is 32-bit float, so a plugin that peaks over 1.0 is measured, never clipped. For a Roland machine, `--dt1-head "41 10 00 06" --dt1-base 1004000` sends DT1: `--dt1 OFF=VAL` pins a value, `--sweep-dt1 OFF:STEPS[:MAX]` and a repeated `--axis` sweep, `--probe LO-HI[:V1,V2]` finds which offset is a control, and `--map LO-HI[:VAL]` asks the plugin which parameter an offset is.
21
+
22
+ <constraints>
23
+ Always do:
24
+ 1. Run `--list` first. `--param` and `--sweep` take the id, never the row number. A wrong id is ignored, and the sweep renders the same sound with no complaint.
25
+ 2. Prove the bounce before you trust it. Render twice: same bytes means the plugin is repeatable, and you can compare exactly. If it is not (a free-running oscillator, modelled drift), compare averages of several bounces, and say so.
26
+ 3. Prove a control reaches the audio: sweep it, and measure that the audio moves. Two renders that match byte for byte mean the control did nothing.
27
+ 4. Render one note per value. Move a control between notes, never under one.
28
+ 5. Give a firmware emulator its boot: `--boot 12`. The emulated processor needs real samples before it answers a note, and a short boot reads as a silent plugin.
29
+ 6. When parameters do nothing, try sysex. A firmware emulator may ignore host automation and answer only the machine's own messages. The renderer sets both the controller and the processor's queue, and if the audio is still byte-identical, sysex is the way in.
30
+ 7. Ask whether a dump was taken: `--dump-params` before and after. A rejected dump and an accepted one that sounds alike cannot be told apart in a WAV. Identical output means the patches never arrived, whatever the spectra look like.
31
+ 8. Fix everything else: patch, note, velocity, sample rate, effects off. To fit one control, move only that control. A factory-patch recording cannot fit a law, since a performance has its chorus on.
32
+ 9. To compare two engines, render both with the same patch and the same notes, and name in the report what the other engine is. An emulation is a reference, not the machine: tag it as what it is, and record where it disagrees with the schematic rather than choosing quietly.
33
+
34
+ Never do:
35
+ - Open an audio device, or route through a loopback, to capture a plugin. On macOS a permission denial returns silence, not an error.
36
+ - Load a plugin in a JUCE host to measure it and trust a crash as the plugin's fault: an allocator clash across the boundary aborts inside the plugin's own loader.
37
+ - Read one output bus as all of them. The renderer gives every bus a buffer and keeps bus 0: a plugin with several may send a part elsewhere.
38
+ - Commit a render: it is a build output, and a plugin's sound may be someone else's. Keep the numbers.
39
+ - Run a cracked or unlicensed plugin. Use the maker's trial.
40
+ </constraints>
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # Builds the offline VST3 renderer on macOS or Linux. It links the VST3 SDK a project's JUCE already holds, and must not link JUCE itself: that is the
3
+ # whole point, since a JUCE host cannot load every JUCE plugin. Run it from the project root, after the project's first configure has fetched JUCE.
4
+ # bash .claude/tools/vst3host/build.sh [out-dir] VST3_SDK=<dir> names the SDK; otherwise it is looked for under JUCE
5
+ # The source list took finding, so it is written here: the SDK inside JUCE is trimmed, and each of these has to be named.
6
+ set -euo pipefail
7
+
8
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ out="${1:-out/vst3host}"
10
+
11
+ find_sdk() {
12
+ local root module
13
+ if [[ -n "${VST3_SDK:-}" ]]; then
14
+ printf '%s\n' "$VST3_SDK"
15
+ return 0
16
+ fi
17
+ for root in out/*/_deps/juce-src build/_deps/juce-src build/*/_deps/juce-src cmake-build-*/_deps/juce-src JUCE third_party/JUCE; do
18
+ for module in juce_audio_processors_headless juce_audio_processors; do
19
+ if [[ -d "$root/modules/$module/format_types/VST3_SDK/public.sdk" ]]; then
20
+ printf '%s\n' "$PWD/$root/modules/$module/format_types/VST3_SDK"
21
+ return 0
22
+ fi
23
+ done
24
+ done
25
+ return 1
26
+ }
27
+
28
+ if ! sdk="$(find_sdk)"; then
29
+ echo "build.sh: no VST3 SDK found. Configure the project so JUCE is fetched, or set VST3_SDK to the folder." >&2
30
+ exit 1
31
+ fi
32
+
33
+ sources=(
34
+ public.sdk/source/vst/hosting/module.cpp
35
+ public.sdk/source/vst/hosting/hostclasses.cpp
36
+ public.sdk/source/vst/hosting/pluginterfacesupport.cpp
37
+ public.sdk/source/vst/vstinitiids.cpp
38
+ public.sdk/source/vst/utility/stringconvert.cpp
39
+ public.sdk/source/common/commonstringconvert.cpp
40
+ public.sdk/source/common/memorystream.cpp
41
+ pluginterfaces/base/funknown.cpp
42
+ pluginterfaces/base/coreiids.cpp
43
+ pluginterfaces/base/conststringtable.cpp
44
+ pluginterfaces/base/ustring.cpp
45
+ base/source/fobject.cpp
46
+ base/source/fstring.cpp
47
+ base/source/fdebug.cpp
48
+ base/source/fbuffer.cpp
49
+ base/source/baseiids.cpp
50
+ base/source/fstreamer.cpp
51
+ base/thread/source/flock.cpp
52
+ )
53
+ paths=()
54
+ for source in "${sources[@]}"; do
55
+ paths+=("$sdk/$source")
56
+ done
57
+
58
+ mkdir -p "$out"
59
+ flags=(-std=c++17 -O2 -DRELEASE=1 -I"$sdk" -I"$here")
60
+ case "$(uname -s)" in
61
+ Darwin)
62
+ clang++ "${flags[@]}" -fobjc-arc -c "$sdk/public.sdk/source/vst/hosting/module_mac.mm" -o "$out/module_mac.o"
63
+ clang++ "${flags[@]}" "$here/render.cpp" "$out/module_mac.o" "${paths[@]}" -framework CoreFoundation -framework Foundation -o "$out/render"
64
+ ;;
65
+ Linux)
66
+ "${CXX:-g++}" "${flags[@]}" "$here/render.cpp" "$sdk/public.sdk/source/vst/hosting/module_linux.cpp" "${paths[@]}" -ldl -lpthread -o "$out/render"
67
+ ;;
68
+ *)
69
+ echo "build.sh: macOS and Linux only. On Windows, build render.cpp with module_win32.cpp and the same sources." >&2
70
+ exit 1
71
+ ;;
72
+ esac
73
+ echo "built $out/render"
@@ -0,0 +1,101 @@
1
+ // What a VST3 host must supply and the SDK inside JUCE leaves out: the parameter and event lists, and the wire between a plugin's two halves.
2
+ #pragma once
3
+
4
+ #include "pluginterfaces/vst/ivstevents.h"
5
+ #include "pluginterfaces/vst/ivstmessage.h"
6
+ #include "pluginterfaces/vst/ivstparameterchanges.h"
7
+
8
+ #include <utility>
9
+ #include <vector>
10
+
11
+ namespace vst3host {
12
+
13
+ using namespace Steinberg;
14
+
15
+ // One parameter's points for one block.
16
+ class ParamQueue final : public Vst::IParamValueQueue {
17
+ public:
18
+ explicit ParamQueue (Vst::ParamID id) : pid (id) {}
19
+ Vst::ParamID PLUGIN_API getParameterId() override { return pid; }
20
+ int32 PLUGIN_API getPointCount() override { return (int32) points.size(); }
21
+ tresult PLUGIN_API getPoint (int32 i, int32& offset, Vst::ParamValue& value) override {
22
+ if (i < 0 || i >= (int32) points.size()) return kResultFalse;
23
+ offset = points[(size_t) i].first;
24
+ value = points[(size_t) i].second;
25
+ return kResultOk;
26
+ }
27
+ tresult PLUGIN_API addPoint (int32 offset, Vst::ParamValue value, int32& i) override {
28
+ points.emplace_back (offset, value);
29
+ i = (int32) points.size() - 1;
30
+ return kResultOk;
31
+ }
32
+ DECLARE_FUNKNOWN_METHODS
33
+ private:
34
+ Vst::ParamID pid;
35
+ std::vector<std::pair<int32, Vst::ParamValue>> points;
36
+ };
37
+ IMPLEMENT_FUNKNOWN_METHODS (ParamQueue, Vst::IParamValueQueue, Vst::IParamValueQueue::iid)
38
+
39
+ class ParamChanges final : public Vst::IParameterChanges {
40
+ public:
41
+ ~ParamChanges() { clear(); }
42
+ int32 PLUGIN_API getParameterCount() override { return (int32) queues.size(); }
43
+ Vst::IParamValueQueue* PLUGIN_API getParameterData (int32 i) override {
44
+ return (i >= 0 && i < (int32) queues.size()) ? queues[(size_t) i] : nullptr;
45
+ }
46
+ Vst::IParamValueQueue* PLUGIN_API addParameterData (const Vst::ParamID& id, int32& i) override {
47
+ for (size_t k = 0; k < queues.size(); ++k) {
48
+ if (queues[k]->getParameterId() != id) continue;
49
+ i = (int32) k;
50
+ return queues[k];
51
+ }
52
+ auto* queue = new ParamQueue (id);
53
+ queue->addRef();
54
+ queues.push_back (queue);
55
+ i = (int32) queues.size() - 1;
56
+ return queue;
57
+ }
58
+ void clear() {
59
+ for (auto* queue : queues) queue->release();
60
+ queues.clear();
61
+ }
62
+ DECLARE_FUNKNOWN_METHODS
63
+ private:
64
+ std::vector<ParamQueue*> queues;
65
+ };
66
+ IMPLEMENT_FUNKNOWN_METHODS (ParamChanges, Vst::IParameterChanges, Vst::IParameterChanges::iid)
67
+
68
+ class EventList final : public Vst::IEventList {
69
+ public:
70
+ int32 PLUGIN_API getEventCount() override { return (int32) events.size(); }
71
+ tresult PLUGIN_API getEvent (int32 i, Vst::Event& e) override {
72
+ if (i < 0 || i >= (int32) events.size()) return kResultFalse;
73
+ e = events[(size_t) i];
74
+ return kResultOk;
75
+ }
76
+ tresult PLUGIN_API addEvent (Vst::Event& e) override {
77
+ events.push_back (e);
78
+ return kResultOk;
79
+ }
80
+ void clear() { events.clear(); }
81
+ DECLARE_FUNKNOWN_METHODS
82
+ private:
83
+ std::vector<Vst::Event> events;
84
+ };
85
+ IMPLEMENT_FUNKNOWN_METHODS (EventList, Vst::IEventList, Vst::IEventList::iid)
86
+
87
+ // A plugin's component and controller talk through connection points, and the host is the wire. Some plugins build their parameter list
88
+ // on the controller only after the component tells them to: without this a controller reports no parameters.
89
+ class ConnectionProxy final : public Vst::IConnectionPoint {
90
+ public:
91
+ explicit ConnectionProxy (Vst::IConnectionPoint* target) : other (target) {}
92
+ tresult PLUGIN_API connect (Vst::IConnectionPoint*) override { return kResultOk; }
93
+ tresult PLUGIN_API disconnect (Vst::IConnectionPoint*) override { return kResultOk; }
94
+ tresult PLUGIN_API notify (Vst::IMessage* message) override { return other ? other->notify (message) : kResultFalse; }
95
+ DECLARE_FUNKNOWN_METHODS
96
+ private:
97
+ Vst::IConnectionPoint* other;
98
+ };
99
+ IMPLEMENT_FUNKNOWN_METHODS (ConnectionProxy, Vst::IConnectionPoint, Vst::IConnectionPoint::iid)
100
+
101
+ } // namespace vst3host