diff --git a/CLAUDE.md b/CLAUDE.md index 806a325..cdb53dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,7 +49,6 @@ crates/ ├── maestro/ # umbrella binary `maestro` (cli + daemon + tui, feature-gated) (NAP-245) ├── maestro-protocol/ # shared wire types (NAP-211) ├── maestro-config/ # shared on-disk config schema (NAP-233) -├── maestro-names/ # orchestra-themed session-handle generator (#23) ├── maestro-cli/ # `maestro list/spawn/attach/kill` dispatch (NAP-216) ├── maestro-daemon/ # `maestro daemon` — tmux wrapper + HTTP/WS API (NAP-212, NAP-214) ├── maestro-web/ # standalone `maestro-web` browser UI (NAP-241, NAP-249) @@ -61,9 +60,6 @@ web/ # browser SPA (NAP-218) See `docs/INSTALL.md` for end-user install instructions. -The dated design docs under `docs/superpowers/specs/` predate the single-host -refactor; they carry banners marking what no longer applies. - ## Conventions - **TDD by default.** Failing test first, then minimal impl. Commit test + impl together. diff --git a/Cargo.lock b/Cargo.lock index 605a736..12d1764 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1077,7 +1077,6 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", - "urlencoding", "uuid", ] @@ -1147,13 +1146,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "maestro-names" -version = "0.1.0" -dependencies = [ - "rand 0.8.6", -] - [[package]] name = "maestro-protocol" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 87454a4..b33b5bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,13 +18,11 @@ dirs = "5" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } libc = "0.2" maestro-config = { path = "crates/maestro-config" } -maestro-names = { path = "crates/maestro-names" } maestro-protocol = { path = "crates/maestro-protocol" } maestro-tui = { path = "crates/maestro-tui" } nix = { version = "0.29", default-features = false, features = ["term", "signal"] } rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } -rust-embed = "8" include_dir = "0.7" mime_guess = "2" bytes = "1" diff --git a/crates/maestro-cli/Cargo.toml b/crates/maestro-cli/Cargo.toml index ae360a4..54a6a7a 100644 --- a/crates/maestro-cli/Cargo.toml +++ b/crates/maestro-cli/Cargo.toml @@ -28,7 +28,6 @@ tokio-tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } toml.workspace = true tracing.workspace = true tracing-subscriber.workspace = true -urlencoding = "2" uuid.workspace = true [dev-dependencies] diff --git a/crates/maestro-names/Cargo.toml b/crates/maestro-names/Cargo.toml deleted file mode 100644 index 011cd23..0000000 --- a/crates/maestro-names/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "maestro-names" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -publish.workspace = true -description = "Orchestra-flavoured session-handle generator (adjective × instrument) shared across Maestro" - -[dependencies] -rand.workspace = true - -[dev-dependencies] -rand = { workspace = true, features = ["small_rng"] } diff --git a/crates/maestro-names/src/lib.rs b/crates/maestro-names/src/lib.rs deleted file mode 100644 index 6bc7330..0000000 --- a/crates/maestro-names/src/lib.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! `maestro-names` — the shared session-handle generator. -//! -//! Maestro is a conductor's podium for every shell you care about, so the -//! handles it hands out are drawn from the orchestra pit: a musical adjective -//! (tempo, dynamics, articulation) paired with an instrument or musical form. -//! The result reads like a marking on a score — `allegro-cello`, -//! `crescendo-oboe`, `staccato-viola`. -//! -//! ## Determinism -//! -//! Every pure path takes an **injected** [`rand::Rng`]; nothing here reaches -//! for `thread_rng()` or the clock, so seeded tests are reproducible (a repo -//! invariant). [`generate_thread`] is the one impure convenience wrapper for -//! production callers that just want a name and don't care about the seed. -//! -//! The Rust generator is the source of truth. If the web UI wants an instant -//! client-side reroll it can mirror [`ADJECTIVES`] / [`NOUNS`] verbatim, but -//! the wordlists live here. - -use rand::seq::SliceRandom; -use rand::Rng; - -/// Musical adjectives — tempo, dynamics, and articulation markings. -pub const ADJECTIVES: &[&str] = &[ - "allegro", - "andante", - "adagio", - "presto", - "vivace", - "largo", - "grave", - "lento", - "moderato", - "crescendo", - "staccato", - "legato", - "forte", - "dolce", - "agitato", - "cantabile", - "sostenuto", - "tremolo", - "rubato", - "marcato", - "brillante", - "maestoso", - "grazioso", - "espressivo", - "tranquillo", - "animato", - "spirito", - "sotto", - "vivo", - "feroce", -]; - -/// Musical nouns — instruments and forms from the concert hall. -pub const NOUNS: &[&str] = &[ - "cello", "violin", "viola", "oboe", "flute", "clarinet", "bassoon", "trumpet", "trombone", - "tuba", "horn", "timpani", "harp", "piano", "organ", "lyre", "lute", "fugue", "sonata", - "octave", "chord", "cadence", "prelude", "rondo", "nocturne", "scherzo", "refrain", "coda", - "motif", "overture", -]; - -/// Generate a single `adjective-noun` handle from an injected RNG. -/// -/// ``` -/// use rand::SeedableRng; -/// let mut rng = rand::rngs::SmallRng::seed_from_u64(7); -/// let name = maestro_names::generate(&mut rng); -/// assert!(name.contains('-')); -/// ``` -pub fn generate(rng: &mut R) -> String { - // Both lists are compile-time non-empty (asserted in tests), so the - // `choose` calls cannot return `None`. - let adj = ADJECTIVES.choose(rng).expect("ADJECTIVES is non-empty"); - let noun = NOUNS.choose(rng).expect("NOUNS is non-empty"); - format!("{adj}-{noun}") -} - -/// Generate a handle that isn't already taken. -/// -/// Draws `adjective-noun` candidates from `rng`; if `is_taken` rejects one, a -/// numeric uniqueness suffix is appended and incremented (`-2`, `-3`, …) until -/// a free handle is found. The bare form is preferred — the suffix only -/// appears on collision, matching the score-marking aesthetic. -/// -/// `is_taken` is the single source of "already used"; given the same RNG and -/// the same predicate the output is fully deterministic. -pub fn generate_unique(rng: &mut R, is_taken: F) -> String -where - R: Rng + ?Sized, - F: Fn(&str) -> bool, -{ - let base = generate(rng); - if !is_taken(&base) { - return base; - } - // Start at 2 so the first duplicate reads `name`, `name-2`, `name-3` … - for suffix in 2u64.. { - let candidate = format!("{base}-{suffix}"); - if !is_taken(&candidate) { - return candidate; - } - } - unreachable!("u64 suffix space is effectively inexhaustible") -} - -/// Impure convenience wrapper: generate a handle from the thread RNG. -/// -/// For production callers that just want a name. Tests and any deterministic -/// path should use [`generate`] / [`generate_unique`] with a seeded RNG. -pub fn generate_thread() -> String { - generate(&mut rand::thread_rng()) -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::rngs::SmallRng; - use rand::SeedableRng; - use std::collections::HashSet; - - fn seeded(seed: u64) -> SmallRng { - SmallRng::seed_from_u64(seed) - } - - #[test] - fn wordlists_are_non_empty() { - assert!(!ADJECTIVES.is_empty()); - assert!(!NOUNS.is_empty()); - } - - #[test] - fn wordlists_have_no_duplicates() { - assert_eq!( - ADJECTIVES.iter().collect::>().len(), - ADJECTIVES.len(), - "duplicate adjective" - ); - assert_eq!( - NOUNS.iter().collect::>().len(), - NOUNS.len(), - "duplicate noun" - ); - } - - #[test] - fn generate_is_adjective_dash_noun() { - let mut rng = seeded(1); - let name = generate(&mut rng); - let (adj, noun) = name.split_once('-').expect("must contain a single dash"); - assert!(ADJECTIVES.contains(&adj), "{adj} not an adjective"); - assert!(NOUNS.contains(&noun), "{noun} not a noun"); - } - - #[test] - fn generation_is_deterministic_under_seed() { - // Same seed → identical sequence; this is the core repo invariant. - let mut a = seeded(42); - let mut b = seeded(42); - for _ in 0..50 { - assert_eq!(generate(&mut a), generate(&mut b)); - } - } - - #[test] - fn different_seeds_diverge() { - // Not a guarantee for a single draw, but across a run the sequences - // must differ — otherwise the RNG isn't actually being threaded. - let mut a = seeded(1); - let mut b = seeded(2); - let seq_a: Vec<_> = (0..30).map(|_| generate(&mut a)).collect(); - let seq_b: Vec<_> = (0..30).map(|_| generate(&mut b)).collect(); - assert_ne!(seq_a, seq_b); - } - - #[test] - fn unique_returns_bare_name_when_free() { - let mut rng = seeded(3); - let name = generate_unique(&mut rng, |_| false); - assert!( - !name.ends_with("-2"), - "no suffix expected when nothing taken" - ); - assert!(name.contains('-')); - } - - #[test] - fn unique_appends_incrementing_suffix_on_collision() { - // Reserve the bare name and the first two suffixed variants; the - // generator must walk past them to `-4`. - let mut probe = seeded(9); - let base = generate(&mut probe); - let taken: HashSet = [base.clone(), format!("{base}-2"), format!("{base}-3")] - .into_iter() - .collect(); - - let mut rng = seeded(9); - let name = generate_unique(&mut rng, |n| taken.contains(n)); - assert_eq!(name, format!("{base}-4")); - } - - #[test] - fn unique_suffix_starts_at_two() { - let mut probe = seeded(11); - let base = generate(&mut probe); - let taken: HashSet = [base.clone()].into_iter().collect(); - - let mut rng = seeded(11); - let name = generate_unique(&mut rng, |n| taken.contains(n)); - assert_eq!(name, format!("{base}-2")); - } -} diff --git a/web/src/names.ts b/web/src/names.ts index 1fcf15a..9de5d70 100644 --- a/web/src/names.ts +++ b/web/src/names.ts @@ -1,7 +1,6 @@ // Client-side session-handle generator for the spawn form's instant reroll -// (#23/#27). Mirrors the wordlists in `crates/maestro-names/src/lib.rs` verbatim -// — the Rust generator is the source of truth, but the web wants a zero-round- -// trip reroll, so the lists are duplicated here. Keep them in sync. +// (#23/#27). The spawn form rerolls without a round trip, so the wordlists +// live here; nothing server-side generates handles. /** Musical adjectives — tempo, dynamics, and articulation markings. */ const ADJECTIVES = [