···9090| **P8 — Delete the C + retire the oracle harness ✅ DONE** (per-sub-phase: [`PLAN-P8-progress.md`](PLAN-P8-progress.md) + [`docs/progress-log/p8.md`](docs/progress-log/p8.md)) | remaining C (the ~25 variadic sender trampolines + residual data globals) + `ircd-sys` C build + `ircd-golden` (L2) + `ircd-testkit`/`cref_*` archives (L1) + the frozen reference‑C tree | With all C _logic_ already Rust (P7 exit), delete the last C: replace the variadic sender trampolines with a non‑variadic Rust sender API at every call site (faithful `wire!`/`reply_one!`/`format!` first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen `extern` decls resolve to `#[no_mangle]` Rust defs at link), then drop **all** C compilation from `ircd-sys` (no more `cc::Build`), retire the differential harness (`cref_*`/L1 `ircd-testkit`/`ircd-golden` L2 can no longer be built once the oracle is gone), and **migrate the load‑bearing tests into `ircd-common`** as self‑contained Rust tests (own fixtures, no `cref_` oracle). **Progress (P8a–P8r — full entries in the progress logs):** **every variadic sender trampoline is now Rust** (P8a–P8m, incl. the keystones `sendto_one`/`sendto_flag`/`esendto_*`), and the five data‑global `.o`s are dropped outright (`s_auth.o` iauth globals P8n, `send.o` P8o, `s_bsd.o` `local[]`/`highest_fd`/… P8p, `ircd.o` `me`/`client`/timers/… P8q). **P8r dropped the last C‑logic TU `support.o`** — `dgets`/`make_isupport` ported to Rust + the `ipv6string`/`minus_one` data globals defined as `#[no_mangle]` statics (`snprintf_append` is dead in Rust — WHOX is field‑by‑field — so it died with the `.o`; `irc_sprintf` is deleted not ported, in P11). **Every ircd C TU is now Rust:** the link set holds only the generated `version.o` + the L1 harness's `ctruth.o`. **P8s migrated all 115 L1 `cref_` differentials into self‑contained `ircd-common` `insta` snapshot tests** (drive only the Rust port via `link_anchor()`, no oracle; soundness via the capture chain) — the suite is green + 0 warnings. **P8t ported the last *generated* C TU `version.c` to Rust** (`ircd-common/src/version.rs`): the data globals `generation`/`creation`/`pass_version`/`infotext`/`isupport` are now `#[no_mangle]` statics via the data‑symbol seam — `generation`/`creation` sourced from crate metadata at build time (`CARGO_PKG_VERSION` + an `ircd-common/build.rs` UTC build stamp), `pass_version`/`infotext` copied verbatim. `version.c.SH` generation + the `version.o` compile/archive are gone from `ircd-sys/build.rs`; `libircd_c.a` now holds **only** the L1 harness `ctruth.o`. (`creation` is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) **P8u (FINAL) retired the oracle.** Ran the differential suite a last time green — L1 `ircd-testkit` all pass; L2 `ircd-golden` 86 pass + only the documented reference‑C‑garbage `s_serv_stats` flake (where Rust is the *correct* side) — then **dropped all C compilation** from `ircd-sys/build.rs`: the `make` object build, the `cref_*`/`ctruth.o`/`res.o` recompiles, the `libircd_c.a` archive, and the whole‑archive link + `-lz`/`-lm`/`-lcrypt` (none needed by Rust — `pow` resolves from glibc 2.29+). `build.rs` now only runs `configure` + bindgen over the C *headers* (the struct view `ircd-common` still uses until P9–P12) and expands the install‑path Makefile vars. `ircd-testkit` (L1) + `ircd-golden` (L2) are `exclude`d from the workspace (mothballed, git‑recoverable); the `ctruth.c`/`layout.rs` drift‑net is deleted. **The workspace is now 100% Rust — zero C TUs compiled.** **P8v (literal end of C) deleted the C *source tree* itself + retired `ircd-sys`:** froze the generated `bindings.rs` into a committed `ircd-common/src/bindings.rs` (a self‑alias `extern crate self as ircd_sys;` keeps every `ircd_sys::bindings::*` path resolving, zero src churn) + the install‑path consts as literals, then deleted `ircd-sys`, the mothballed `ircd-testkit`/`ircd-golden` oracle crates, the `iauth-rs` C‑iauth differential, and **all** C source (`common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml`). `ircd-rs` calls `ircd_common::ircd::c_ircd_main` directly. **Not one line of C remains in the repo.** | **MET. Final differential run green; oracle mothballed; `cargo build --workspace` 0 warnings links pure Rust; `cargo test --workspace` 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the `ircd-common` snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level.** |
9191| **P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08)** | — (was: whole Rust tree) | **Skipped by decision — superseded by the `leveva` greenfield.** P9 assumed an *in‑place* idiomatic transformation of the mechanical port (`MyMalloc`/`MyFree`→`Box`/`Vec`, `dbuf` freelist→owned, raw `libc`→`std`/`nix`, `[c_char;N]`→`[u8;N]`). Because the idiomatic end‑state is now a separate greenfield crate (`leveva`), `ircd-common` is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. | n/a — retired. |
9292| **P10 — `leveva` greenfield foundations 🔶 IN PROGRESS** ([`docs/progress-log/p10.md`](docs/progress-log/p10.md)) | new `leveva` crate (lib) + `leveva-integration` differential tests | The idiomatic‑Rust product, built from scratch (not transformed from the port). **Done so far:** RFC 1459 case‑folding (`casemap`); typed identifier strings (`IrcStr`/`IrcString` + `Nick`/`ChanName`/`Uid`/`Sid`/`Cid`/`ServerName`/`UserName`/`HostName` validating newtypes via `ident_newtype!`); `Message`/`MessageBuilder`; the full `Numeric` reply/error enum (184 codes, discriminant = wire code); a generic safe `patricia` trie; glob `matching`; `mode`; KDL `config` (model/parse/password/privilege); `rustls`/`tokio` `tls` endpoint with hot reload; `ident`. The async boot path (`main.rs`) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). | `cargo test -p leveva` green + clippy clean per module; `leveva-integration` pins each greenfield reimplementation against `ircd-common` (the oracle) within their shared domain (documented divergences asserted explicitly). |
9393-| **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 291 slices** (291 = FINDFORWARDS: charybdis-style reverse `+f` lookup (`extensions/m_findforwards.c`) — `FINDFORWARDS <channel>` lists every channel whose `+f` forward target (slice 242) is `<channel>`, the companion to the `+f` forwarding family (where `+f` points a *source* at a *target*, FINDFORWARDS asks the target "who points at me?"); new pure read seam `Channels::forwards_to(target)` (folds + scans the channel table, returns matching display names folded-sorted — the fuzz seam), `command/findforwards.rs` with the trace/ban-family gate ordering (`461` no-arg first → `403` nonexistent channel → `442` non-member-non-oper, the oper-bit bypass replacing charybdis's `IsOperSpy` à la CHANTRACE), output as charybdis-faithful `NOTICE` lines (chunked space-separated forwarder list + `End of forwards for <chan>`, or `No channels forward to <chan>` when empty) **not** numerics, `FINDFORWARDS.md` help + COMMANDS allowlist + index.md; leveva-native (no oracle — 2.11 has no FINDFORWARDS), local-only (a read of this server's channel table, mirroring charybdis's per-server `+f` state); `forwards_to` unit + 8 command units + `findforwards_proptest` (2 props: `forwards_to_is_exactly_the_pointing_channels` model set-equality + `arbitrary_args_never_panic`) + `golden_findforwards`; 290 = REMOVE: charybdis-style chanop force-part (`extensions/m_remove.c`) — a channel operator forcibly removes a member who, on the wire, appears to **PART** rather than be **KICK**ed (so client auto-rejoin-on-kick scripts don't fire); `command/remove.rs` reuses `Channels::kick_gate`/`kick` verbatim so it **inherits every KICK rule** (`403`/`442`/`482` chanop gate, `401`/`441`, elemental rank protection, `+Y` official-join immunity, cross-product, `461`), but emits `:victim!user@host PART <chan> :requested by <remover> [(<reason>)]` (prefix = the **victim's** own mask via `Registry::record_of`, works local+remote) echoed to the remover + delivered to all local members incl. the victim, propagated S2S as the victim's PART (`s2s::relay::local_part`) and recorded to chat-history as a PART; a normal chanop command (no oper privilege), the sibling of KICK/the SA*-force family; leveva-native (the C oracle has no REMOVE) → no differential, the "requested by" wording is leveva's; `REMOVE.md` help + COMMANDS allowlist + index.md; `golden_remove` + `remove_proptest` (2 props); 289 = XLINE/UNXLINE: charybdis realname (gecos) operator ban — the realname sibling of K-line/D-line, completing the runtime ban quartet **K/D/X/RESV**; bolt-for-bolt the DLINE plane (slice 284) with a single-token gecos matcher (`matching::matches`) instead of CIDR — `leveva::xline::{Xline,XlineStore}` (SQLite write-through + boot reload), `command/xline.rs` (461→`OperPrivilege::Xline` 481, reap local matching realnames with 465+ERROR), `s2s/xline.rs` `ENCAP * XLINE/UNXLINE` propagation + `xline_burst` re-assertion, registration X-line gate in `finalize_registration` after the K-line/config-`ban` gates, `STATS x`→`247 RPL_STATSXLINE`; **lights up the dead `xline_exempt` flag** end-to-end (`Admission`/`ClientRecord.xline_exempt` + `set_xline_exempt`, the dedicated exemption distinct from `kline-exempt`); divergences = single-token-glob mask (vs charybdis spaced gecos) + dedicated xline-exempt + leveva-native no-differential; `golden_xline` + `xline_proptest` (6 props); 288 = TESTMASK: `TESTMASK <[nick!]user@host> [<gecos>]` — a charybdis-style read-only operator diagnostic that **counts** how many connected clients match a hostmask (and optional realname glob), split into local vs remote; the population sibling of TESTLINE (285, which reports *bans*), together completing charybdis's `test*` diagnostic family. `461` param gate before the plain oper-bit gate (`481`); a malformed mask (no `@`, or empty user/host) → `NOTICE :Invalid parameters` (faithful, not a numeric); a registry sweep glob-matches `nick`/`user`/`host`(+`orighost`)/`gecos` (nick & gecos default `*`) and tallies `is_local_uid` → one `727 RPL_TESTMASKGECOS` `<lcount> <gcount> <nick>!<user>@<host> <gecos> :Local/remote clients match`; new numeric 727 (charybdis's 724 is dead — no format/caller), pure `parse_mask` fuzz seam, `TESTMASK.md` help; leveva-native, local-only (no S2S, mirroring charybdis); confirmed against cloned charybdis `m_testmask.c`/`messages.h`; 287 = CHANTRACE: `CHANTRACE <#channel>` — the channel-scoped sibling of TRACE (26)/ETRACE/MASKTRACE (286), completing the trace quartet: reports every member of a named channel in ETRACE's extended `708 RPL_ETRACEFULL` column format, folded-nick order, closing `262 RPL_TRACEEND`; gate is `461` (missing channel) → `403` (no such channel, existence before membership) → `442` unless the requester is a member **or** holds `OperPrivilege::Trace` (the operspy-equivalent bypass; leveva has no operspy `!` prefix); reuses `etrace::etrace_line`, no new numeric; `CHANTRACE.md` help; leveva-native (2.11 has no CHANTRACE), reports the **full roster incl. remote members** (unlike local-only ETRACE/MASKTRACE) but no S2S propagation, no IP-hiding (host==host, no separate IP column); 286 = MASKTRACE: `MASKTRACE <nick!user@host mask> [<gecos mask>]` — a charybdis-style mask-filtered extended trace, the sibling of TRACE (26) and ETRACE: reports every local client whose `nick!user@host` glob-matches (and optionally whose realname matches a 2nd gecos glob) in ETRACE's extended column format; `461` param gate before the `OperPrivilege::Trace` oper gate (`481`), one reused `708 RPL_ETRACEFULL` line per match in folded-nick order via `matching::HostMask` + the promoted `etrace::etrace_line`, closing with `262 RPL_TRACEEND` (no new numeric); `MASKTRACE.md` help; leveva-native (2.11 has no MASKTRACE), local-only (no S2S); 285 = TESTLINE: `TESTLINE <[nick!]user@host | ip/cidr | nick | #channel>` — a charybdis-style **read-only** operator diagnostic, the capstone of the ban family (RESV 272 / TKLINE 281 / DLINE 284): it reports which **active** ban would match a mask, laying/lifting nothing. Param gate (`461`) then a plain oper-bit gate (any oper, no per-ban privilege → `481`); a pure `probe()` classifier routes by mask shape (`user@host`→D-line-then-K-line, `#channel`→channel RESV, bare token→D-line-as-IP-then-nick-RESV) and reports the first hit in priority **D > K > R** as `725 RPL_TESTLINE` (`<type K/D/R> <minutes-remaining, 0 for permanent RESV> <ban-mask> :<reason>`), else `726 RPL_NOTESTLINE`; expiry honoured via `find_active`; new numerics 725/726, `TESTLINE.md` help; leveva-native (no oracle — 2.11 has no TESTLINE), single-best-match, no I-line arm, **local-only** (no S2S, mirroring charybdis); 284 = D-lines: `DLINE <duration> <ip/cidr> [:reason]` / `UNDLINE <ip/cidr>` — charybdis-style IP-level operator bans, the IP/CIDR sibling of the temporary K-line family (281–283); new `DlineStore` (mirrors `KlineStore`, CIDR-aware `host_component_matches`, same `database {}` SQLite write-through + boot reload), `OperPrivilege::Dline` (bit `0x400000`), registration gate **before** the K-line gate matching the pre-cloak connect IP (`465`+`ERROR`+REJ snomask, exemption reuses `kline-exempt`), local reap by `orighost`, S2S `ENCAP * DLINE`/`UNDLINE` propagation + burst re-assertion (slice-281 KLINE shape), `STATS d` → `250 RPL_STATSDLINE`, `DLINE.md`/`UNDLINE.md` help; leveva-native, no oracle differential; 283 = SQLite persistence for permanent (`+P`) channels — a `+P` channel (slice 244, survives empty) now also survives a **restart**, persisting to the same `database {}` SQLite file that backs chat-history (274) and K-lines/reservations (282): its identity + `created_at` TS + full `ChannelModes` + topic write through to a `channel` table (JSON `modes`/`topic` columns, the serde codec chat-history uses) on every durable mutation, via the single `Channels::reconcile_persist` chokepoint (upsert when `+P`, **delete on `-P`**) hooked into all eight durable-state mutators — `apply_modes`/`apply_modes_oper`/`apply_modes_as_server` (MODE incl. the `+P`/`-P` toggle, key, limit, all `+b/+e/+I/+R` lists), `reset_flag_state`/`wipe_list_masks` (CHANTS-merge), `set_topic`/`set_topic_as_server` (TOPIC), and `create_permanent_from_burst` (an empty `+P` channel learned from a peer burst persists so it outlives *this* server's restart too, mirroring 282's inbound-S2S persist); recreated **empty** at boot (members/INVITE-tokens/join-throttle counters never persist — an empty `+P` channel is exactly what 244/269 make valid), with `created_at` carried so TS6 merge arbitration is stable post-reboot; a `persisted: HashSet` of folded keys makes a `reconcile_persist` on a never-persisted (non-`+P`) channel a pure in-memory miss — **no disk write per MODE on an ordinary channel** — and the two leaf locks (`db`/`persisted`) are never held simultaneously so there is no lock-order hazard with the held `by_name` guard; a `new()`/no-`database{}` table stays in-memory only (every persistence path a no-op); `ChannelModes`/`Topic` gained `#[derive(Serialize, Deserialize)]`; `from_config` opens the table against `cfg.database.path` (log + fall back to in-memory on error, never fatal), exactly the 282 klines/resvs shape; 282 = SQLite persistence for K-lines + reservations — operator-laid `TKLINE` bans and `RESV` reservations (lost on restart before) now persist to the **same `database {}` SQLite file** that backs chat-history (slice 274), so they survive a reboot: each `KlineStore`/`ResvStore` gains an optional write-through connection (`open(path)` creates a `kline`/`resv` table + loads existing rows; `add`/`remove`/`prune` mirror to it; a `new()`/no-`database{}` store stays in-memory only, every persistence path a no-op), the in-memory `Vec` staying the hot-path authority; K-lines drop `expires <= now` rows on load + prune; only **runtime** (`conf == false`) reservations persist (config `resv {}` re-derives from `ircd.kdl`, so `seed_config` bypasses persistence and a `REHASH` never drops a learned reservation); `COLLATE NOCASE` primary keys make a case-differing re-add upsert not duplicate; open/write errors log and fall back to in-memory (never fatal); inbound S2S `apply_encap_kline`/`apply_encap_resv` persist what a server learns from the network too (all via `add`); 281 = S2S `KLINE`/`UNKLINE` propagation — the K-line analogue of slice 280: a `TKLINE`/`UNTKLINE` (leveva's temporary K-line family, there is no separate permanent `KLINE`) now propagates network-wide as `ENCAP * KLINE <duration> <user> <host> :<reason>`/`ENCAP * UNKLINE <user> <host>` (server-prefixed, leveva-native, the **remaining** seconds on the wire so each server re-clocks `expires = now + duration` against its own clock — no absolute-timestamp skew), wired into `command/tkline.rs` after the param/privilege/format gates; every server applies the ban to its own `KlineStore` (gating future registrations) **and reaps its own local matching clients** via the now-`is_local_uid`-guarded `reap_matching` helper (which also fixed a latent over-reap of remote users in the local `TKLINE`); `kline-exempt` clients are spared; `UNKLINE` lifts the gate only (never un-kills) and propagates unconditionally; re-asserted to a freshly-linked peer by `s2s/burst.rs::kline_burst` (active bans only, with remaining time; expired bursts nothing); new `s2s/kline.rs` modelled on `s2s/resv.rs`, ENCAP dispatch in `s2s/forward.rs`; 280 = S2S `RESV`/`UNRESV` propagation — an operator-laid nick/channel reservation (and its lift) now propagates network-wide as `ENCAP * RESV`/`UNRESV` (server-prefixed, kind re-derived from the mask prefix on inbound apply), wired into `command/resv.rs` after the param/privilege gates (a `461`/`481` reject propagates nothing; `UNRESV` propagates unconditionally), applied + split-horizon-relayed by `s2s/forward.rs`, and re-asserted to a freshly-linked peer via `s2s/burst.rs::resv_burst` (runtime `conf == false` reservations only — config `resv {}` blocks stay per-server local policy); closes the slice-272/273 S2S-propagation follow-on, mirroring the METADATA/KNOCK ENCAP carriers; 279 = remote-originated JOIN/PART/QUIT membership events are now recorded into chat-history — the membership-event parallel to slice 276 (which did PRIVMSG/NOTICE): a **live** `NJOIN` (`s2s/njoin.rs`, never a burst — bursts are bulk state-sync), a bare `JOIN`/`PART` (`s2s/relay.rs`), and a **discrete** remote `QUIT`/`KILL` (`s2s/squit.rs::quit_one_remote_user`, *not* a netsplit mass teardown) each record an event keyed by a locally-minted msgid + clock, gated on `has_local_member` (only a local member can `CHATHISTORY` it; a pure hub stores nothing); an unmirrored `NJOIN`-only ghost has no mask so its QUIT is skipped; replay/query side unchanged (slice 274 already gates these behind `draft/event-playback`); 278 = `+R` auto-reop is now enforced for **local** members only — the `enforce_reop` chokepoint gained an explicit `is_local_uid` filter, so a `+R` mask matching a `UNICK`-introduced remote user's `nick!user@host` no longer wrongly ops it locally and relays a `+o` we have no authority to emit (the locality guarantee previously rested on the false premise that remote members lack a registry record); 277 = netsplit batches are no longer duplicated to `batch`-capable clients under concurrent teardown — when a dying peer is detected on two paths at once (abrupt socket drop + a sibling's inbound `SQUIT`, in separate tasks), the `NJOIN`-only ghost-member sweep raced (`co_members` then `remove_everywhere`, no single-shot gate) and double-relayed a casualty's `QUIT`; new atomic `Channels::detach_member` fuses recipient-gather + removal in one critical section so only the first teardown to reach a member relays it (the loser gets `None`), mirroring the already-atomic `remove_behind_sid` mirror drain; new `s2s_netsplit_concurrency_proptest` fuzz; 276 = remote-originated channel messages are now recorded into chat-history — the S2S relay's inbound channel fan (`s2s::relay::inbound_message`) records the delivered line on this server too, keyed by the carried network msgid (slice 130) + `@time` (slice 134), gated on this server having a local member of the channel (only a local member can `CHATHISTORY` it; no whole-network accumulation on a hub); the `+z` op-redirect/non-channel branches never record, mirroring the local plane; closes the slice-274 gap where `CHATHISTORY` held only locally-sent lines; 275 = IRCv3 `message-redaction` now deletes from chat-history — an accepted channel `REDACT <chan> <msgid>` deletes the slice-274 stored line (`History::delete_by_msgid`, keyed `(target, msgid)`) so `CHATHISTORY` can never replay it; wired in both the local `CanSend::Ok` path and inbound `ENCAP * REDACT`, per-server; mirrors recording — a `+z` op-moderated/"shown to ops" attempt is never recorded so never deleted; authorization stays a stateless relay; 274 = IRCv3 `draft/chathistory` — SQLite-backed (`rusqlite`, bundled) channel message history behind an optional `ircd.kdl` `database {}` block; all channels auto-enrolled (PRIVMSG/NOTICE + JOIN/PART/QUIT recorded at the delivery plane), joining clients auto-replayed `replay-lines` recent lines unless they negotiated `draft/chathistory` (then exempt, per spec), full `CHATHISTORY LATEST/BEFORE/AFTER/BETWEEN/AROUND/TARGETS` (timestamp=/msgid= selectors) framed as a `chathistory` batch, `draft/chathistory`+`draft/event-playback` dynamic caps + `CHATHISTORY`/`MSGREFTYPES` ISUPPORT, age-based `retention-days` prune ticker; `Message` gained serde; 273 = registration-time nick `RESV` enforcement — a connection may no longer *register* directly as a reserved nick, only the post-registration `NICK`/JOIN were gated before; refuse `432` + reopen registration, mirroring the `433` nick-in-use path; 272 = charybdis `RESV`/`UNRESV` nick/channel reservation — JOIN→`437`/NICK→`432` gates, opers exempt, new `OperPrivilege::Resv`; defaults declared in `resv {}` config blocks, boot-seeded + REHASH-re-seeded keeping runtime `RESV`s). The slices span the full client command surface (registration; channels + the complete channel-mode surface; WHO/WHOIS/WHOX; OPER/STATS; and the query/util commands incl. OPERWALL/LOCOPS/USERIP); the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration); `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident); config live-rehash across every block; CertFP; the **IRCv3 cap track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track); elemental channel modes + extbans; KNOCK; command-rate flood protection (fakelag); the **complete charybdis channel-mode set** (`+C/+c/+S/+j/+z/+T/+g/+r/+f/+Q/+F/+P/+L`, with `+L` the last standard one) and **user-mode set** (`+D/+R/+Q/+G/+z/+S/+l`, plus the product decision that leveva has no local-only umodes — `SEND_UMODES` is every umode); and the `+s` server-notice **snomask** (a charybdis category mask with a producer wired for every category — the matrix is complete). Most const limits (`max-penalty`/`max-bans`/`service-string`/`default-snomask`/…) have been promoted to REHASH-able `options { … }` knobs. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. |
9393+| **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 292 slices** (292 = OPME: charybdis-style operator self-op of an *opless* channel (`extensions/m_opme.c`) — `OPME <channel>` grants the requesting operator `+o` **only** when the channel currently has no operators, the recovery tool for a channel whose last op left and the constrained, safety-gated sibling of `SAMODE +o`; completes the charybdis oper channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb; new atomic seam `Channels::opme(name, uid) -> OpmeOutcome` (single-lock channel-missing→`NoSuchChannel` / non-member→`NotOnChannel` / has-an-op→`NotOpless` / else grant `+o`→`Opped`, the fuzz seam, no persistence — member status never persists), `command/opme.rs` gate ladder (non-oper `481` → no-arg `461` → `403`/`442`/not-opless `NOTICE`/success), on success broadcasts `:<server> MODE <chan> +o <nick>` to local members + relays server-sourced via `s2s::relay::server_channel_mode` + posts a `+s` audit snomask `<oper> used OPME on <chan>`; divergences = leveva-native (no oracle), membership-checked-before-opless (vs charybdis opless-first, so a non-member never learns op state), `+s` audit snomask not `+w` wallops (SA*/OJOIN family precedent), local-only; `OPME.md` help + COMMANDS allowlist + index.md operator line; `channel::opme` unit + 4 command units + `opme_proptest` (2 props) + `golden_opme`; 291 = FINDFORWARDS: charybdis-style reverse `+f` lookup (`extensions/m_findforwards.c`) — `FINDFORWARDS <channel>` lists every channel whose `+f` forward target (slice 242) is `<channel>`, the companion to the `+f` forwarding family (where `+f` points a *source* at a *target*, FINDFORWARDS asks the target "who points at me?"); new pure read seam `Channels::forwards_to(target)` (folds + scans the channel table, returns matching display names folded-sorted — the fuzz seam), `command/findforwards.rs` with the trace/ban-family gate ordering (`461` no-arg first → `403` nonexistent channel → `442` non-member-non-oper, the oper-bit bypass replacing charybdis's `IsOperSpy` à la CHANTRACE), output as charybdis-faithful `NOTICE` lines (chunked space-separated forwarder list + `End of forwards for <chan>`, or `No channels forward to <chan>` when empty) **not** numerics, `FINDFORWARDS.md` help + COMMANDS allowlist + index.md; leveva-native (no oracle — 2.11 has no FINDFORWARDS), local-only (a read of this server's channel table, mirroring charybdis's per-server `+f` state); `forwards_to` unit + 8 command units + `findforwards_proptest` (2 props: `forwards_to_is_exactly_the_pointing_channels` model set-equality + `arbitrary_args_never_panic`) + `golden_findforwards`; 290 = REMOVE: charybdis-style chanop force-part (`extensions/m_remove.c`) — a channel operator forcibly removes a member who, on the wire, appears to **PART** rather than be **KICK**ed (so client auto-rejoin-on-kick scripts don't fire); `command/remove.rs` reuses `Channels::kick_gate`/`kick` verbatim so it **inherits every KICK rule** (`403`/`442`/`482` chanop gate, `401`/`441`, elemental rank protection, `+Y` official-join immunity, cross-product, `461`), but emits `:victim!user@host PART <chan> :requested by <remover> [(<reason>)]` (prefix = the **victim's** own mask via `Registry::record_of`, works local+remote) echoed to the remover + delivered to all local members incl. the victim, propagated S2S as the victim's PART (`s2s::relay::local_part`) and recorded to chat-history as a PART; a normal chanop command (no oper privilege), the sibling of KICK/the SA*-force family; leveva-native (the C oracle has no REMOVE) → no differential, the "requested by" wording is leveva's; `REMOVE.md` help + COMMANDS allowlist + index.md; `golden_remove` + `remove_proptest` (2 props); 289 = XLINE/UNXLINE: charybdis realname (gecos) operator ban — the realname sibling of K-line/D-line, completing the runtime ban quartet **K/D/X/RESV**; bolt-for-bolt the DLINE plane (slice 284) with a single-token gecos matcher (`matching::matches`) instead of CIDR — `leveva::xline::{Xline,XlineStore}` (SQLite write-through + boot reload), `command/xline.rs` (461→`OperPrivilege::Xline` 481, reap local matching realnames with 465+ERROR), `s2s/xline.rs` `ENCAP * XLINE/UNXLINE` propagation + `xline_burst` re-assertion, registration X-line gate in `finalize_registration` after the K-line/config-`ban` gates, `STATS x`→`247 RPL_STATSXLINE`; **lights up the dead `xline_exempt` flag** end-to-end (`Admission`/`ClientRecord.xline_exempt` + `set_xline_exempt`, the dedicated exemption distinct from `kline-exempt`); divergences = single-token-glob mask (vs charybdis spaced gecos) + dedicated xline-exempt + leveva-native no-differential; `golden_xline` + `xline_proptest` (6 props); 288 = TESTMASK: `TESTMASK <[nick!]user@host> [<gecos>]` — a charybdis-style read-only operator diagnostic that **counts** how many connected clients match a hostmask (and optional realname glob), split into local vs remote; the population sibling of TESTLINE (285, which reports *bans*), together completing charybdis's `test*` diagnostic family. `461` param gate before the plain oper-bit gate (`481`); a malformed mask (no `@`, or empty user/host) → `NOTICE :Invalid parameters` (faithful, not a numeric); a registry sweep glob-matches `nick`/`user`/`host`(+`orighost`)/`gecos` (nick & gecos default `*`) and tallies `is_local_uid` → one `727 RPL_TESTMASKGECOS` `<lcount> <gcount> <nick>!<user>@<host> <gecos> :Local/remote clients match`; new numeric 727 (charybdis's 724 is dead — no format/caller), pure `parse_mask` fuzz seam, `TESTMASK.md` help; leveva-native, local-only (no S2S, mirroring charybdis); confirmed against cloned charybdis `m_testmask.c`/`messages.h`; 287 = CHANTRACE: `CHANTRACE <#channel>` — the channel-scoped sibling of TRACE (26)/ETRACE/MASKTRACE (286), completing the trace quartet: reports every member of a named channel in ETRACE's extended `708 RPL_ETRACEFULL` column format, folded-nick order, closing `262 RPL_TRACEEND`; gate is `461` (missing channel) → `403` (no such channel, existence before membership) → `442` unless the requester is a member **or** holds `OperPrivilege::Trace` (the operspy-equivalent bypass; leveva has no operspy `!` prefix); reuses `etrace::etrace_line`, no new numeric; `CHANTRACE.md` help; leveva-native (2.11 has no CHANTRACE), reports the **full roster incl. remote members** (unlike local-only ETRACE/MASKTRACE) but no S2S propagation, no IP-hiding (host==host, no separate IP column); 286 = MASKTRACE: `MASKTRACE <nick!user@host mask> [<gecos mask>]` — a charybdis-style mask-filtered extended trace, the sibling of TRACE (26) and ETRACE: reports every local client whose `nick!user@host` glob-matches (and optionally whose realname matches a 2nd gecos glob) in ETRACE's extended column format; `461` param gate before the `OperPrivilege::Trace` oper gate (`481`), one reused `708 RPL_ETRACEFULL` line per match in folded-nick order via `matching::HostMask` + the promoted `etrace::etrace_line`, closing with `262 RPL_TRACEEND` (no new numeric); `MASKTRACE.md` help; leveva-native (2.11 has no MASKTRACE), local-only (no S2S); 285 = TESTLINE: `TESTLINE <[nick!]user@host | ip/cidr | nick | #channel>` — a charybdis-style **read-only** operator diagnostic, the capstone of the ban family (RESV 272 / TKLINE 281 / DLINE 284): it reports which **active** ban would match a mask, laying/lifting nothing. Param gate (`461`) then a plain oper-bit gate (any oper, no per-ban privilege → `481`); a pure `probe()` classifier routes by mask shape (`user@host`→D-line-then-K-line, `#channel`→channel RESV, bare token→D-line-as-IP-then-nick-RESV) and reports the first hit in priority **D > K > R** as `725 RPL_TESTLINE` (`<type K/D/R> <minutes-remaining, 0 for permanent RESV> <ban-mask> :<reason>`), else `726 RPL_NOTESTLINE`; expiry honoured via `find_active`; new numerics 725/726, `TESTLINE.md` help; leveva-native (no oracle — 2.11 has no TESTLINE), single-best-match, no I-line arm, **local-only** (no S2S, mirroring charybdis); 284 = D-lines: `DLINE <duration> <ip/cidr> [:reason]` / `UNDLINE <ip/cidr>` — charybdis-style IP-level operator bans, the IP/CIDR sibling of the temporary K-line family (281–283); new `DlineStore` (mirrors `KlineStore`, CIDR-aware `host_component_matches`, same `database {}` SQLite write-through + boot reload), `OperPrivilege::Dline` (bit `0x400000`), registration gate **before** the K-line gate matching the pre-cloak connect IP (`465`+`ERROR`+REJ snomask, exemption reuses `kline-exempt`), local reap by `orighost`, S2S `ENCAP * DLINE`/`UNDLINE` propagation + burst re-assertion (slice-281 KLINE shape), `STATS d` → `250 RPL_STATSDLINE`, `DLINE.md`/`UNDLINE.md` help; leveva-native, no oracle differential; 283 = SQLite persistence for permanent (`+P`) channels — a `+P` channel (slice 244, survives empty) now also survives a **restart**, persisting to the same `database {}` SQLite file that backs chat-history (274) and K-lines/reservations (282): its identity + `created_at` TS + full `ChannelModes` + topic write through to a `channel` table (JSON `modes`/`topic` columns, the serde codec chat-history uses) on every durable mutation, via the single `Channels::reconcile_persist` chokepoint (upsert when `+P`, **delete on `-P`**) hooked into all eight durable-state mutators — `apply_modes`/`apply_modes_oper`/`apply_modes_as_server` (MODE incl. the `+P`/`-P` toggle, key, limit, all `+b/+e/+I/+R` lists), `reset_flag_state`/`wipe_list_masks` (CHANTS-merge), `set_topic`/`set_topic_as_server` (TOPIC), and `create_permanent_from_burst` (an empty `+P` channel learned from a peer burst persists so it outlives *this* server's restart too, mirroring 282's inbound-S2S persist); recreated **empty** at boot (members/INVITE-tokens/join-throttle counters never persist — an empty `+P` channel is exactly what 244/269 make valid), with `created_at` carried so TS6 merge arbitration is stable post-reboot; a `persisted: HashSet` of folded keys makes a `reconcile_persist` on a never-persisted (non-`+P`) channel a pure in-memory miss — **no disk write per MODE on an ordinary channel** — and the two leaf locks (`db`/`persisted`) are never held simultaneously so there is no lock-order hazard with the held `by_name` guard; a `new()`/no-`database{}` table stays in-memory only (every persistence path a no-op); `ChannelModes`/`Topic` gained `#[derive(Serialize, Deserialize)]`; `from_config` opens the table against `cfg.database.path` (log + fall back to in-memory on error, never fatal), exactly the 282 klines/resvs shape; 282 = SQLite persistence for K-lines + reservations — operator-laid `TKLINE` bans and `RESV` reservations (lost on restart before) now persist to the **same `database {}` SQLite file** that backs chat-history (slice 274), so they survive a reboot: each `KlineStore`/`ResvStore` gains an optional write-through connection (`open(path)` creates a `kline`/`resv` table + loads existing rows; `add`/`remove`/`prune` mirror to it; a `new()`/no-`database{}` store stays in-memory only, every persistence path a no-op), the in-memory `Vec` staying the hot-path authority; K-lines drop `expires <= now` rows on load + prune; only **runtime** (`conf == false`) reservations persist (config `resv {}` re-derives from `ircd.kdl`, so `seed_config` bypasses persistence and a `REHASH` never drops a learned reservation); `COLLATE NOCASE` primary keys make a case-differing re-add upsert not duplicate; open/write errors log and fall back to in-memory (never fatal); inbound S2S `apply_encap_kline`/`apply_encap_resv` persist what a server learns from the network too (all via `add`); 281 = S2S `KLINE`/`UNKLINE` propagation — the K-line analogue of slice 280: a `TKLINE`/`UNTKLINE` (leveva's temporary K-line family, there is no separate permanent `KLINE`) now propagates network-wide as `ENCAP * KLINE <duration> <user> <host> :<reason>`/`ENCAP * UNKLINE <user> <host>` (server-prefixed, leveva-native, the **remaining** seconds on the wire so each server re-clocks `expires = now + duration` against its own clock — no absolute-timestamp skew), wired into `command/tkline.rs` after the param/privilege/format gates; every server applies the ban to its own `KlineStore` (gating future registrations) **and reaps its own local matching clients** via the now-`is_local_uid`-guarded `reap_matching` helper (which also fixed a latent over-reap of remote users in the local `TKLINE`); `kline-exempt` clients are spared; `UNKLINE` lifts the gate only (never un-kills) and propagates unconditionally; re-asserted to a freshly-linked peer by `s2s/burst.rs::kline_burst` (active bans only, with remaining time; expired bursts nothing); new `s2s/kline.rs` modelled on `s2s/resv.rs`, ENCAP dispatch in `s2s/forward.rs`; 280 = S2S `RESV`/`UNRESV` propagation — an operator-laid nick/channel reservation (and its lift) now propagates network-wide as `ENCAP * RESV`/`UNRESV` (server-prefixed, kind re-derived from the mask prefix on inbound apply), wired into `command/resv.rs` after the param/privilege gates (a `461`/`481` reject propagates nothing; `UNRESV` propagates unconditionally), applied + split-horizon-relayed by `s2s/forward.rs`, and re-asserted to a freshly-linked peer via `s2s/burst.rs::resv_burst` (runtime `conf == false` reservations only — config `resv {}` blocks stay per-server local policy); closes the slice-272/273 S2S-propagation follow-on, mirroring the METADATA/KNOCK ENCAP carriers; 279 = remote-originated JOIN/PART/QUIT membership events are now recorded into chat-history — the membership-event parallel to slice 276 (which did PRIVMSG/NOTICE): a **live** `NJOIN` (`s2s/njoin.rs`, never a burst — bursts are bulk state-sync), a bare `JOIN`/`PART` (`s2s/relay.rs`), and a **discrete** remote `QUIT`/`KILL` (`s2s/squit.rs::quit_one_remote_user`, *not* a netsplit mass teardown) each record an event keyed by a locally-minted msgid + clock, gated on `has_local_member` (only a local member can `CHATHISTORY` it; a pure hub stores nothing); an unmirrored `NJOIN`-only ghost has no mask so its QUIT is skipped; replay/query side unchanged (slice 274 already gates these behind `draft/event-playback`); 278 = `+R` auto-reop is now enforced for **local** members only — the `enforce_reop` chokepoint gained an explicit `is_local_uid` filter, so a `+R` mask matching a `UNICK`-introduced remote user's `nick!user@host` no longer wrongly ops it locally and relays a `+o` we have no authority to emit (the locality guarantee previously rested on the false premise that remote members lack a registry record); 277 = netsplit batches are no longer duplicated to `batch`-capable clients under concurrent teardown — when a dying peer is detected on two paths at once (abrupt socket drop + a sibling's inbound `SQUIT`, in separate tasks), the `NJOIN`-only ghost-member sweep raced (`co_members` then `remove_everywhere`, no single-shot gate) and double-relayed a casualty's `QUIT`; new atomic `Channels::detach_member` fuses recipient-gather + removal in one critical section so only the first teardown to reach a member relays it (the loser gets `None`), mirroring the already-atomic `remove_behind_sid` mirror drain; new `s2s_netsplit_concurrency_proptest` fuzz; 276 = remote-originated channel messages are now recorded into chat-history — the S2S relay's inbound channel fan (`s2s::relay::inbound_message`) records the delivered line on this server too, keyed by the carried network msgid (slice 130) + `@time` (slice 134), gated on this server having a local member of the channel (only a local member can `CHATHISTORY` it; no whole-network accumulation on a hub); the `+z` op-redirect/non-channel branches never record, mirroring the local plane; closes the slice-274 gap where `CHATHISTORY` held only locally-sent lines; 275 = IRCv3 `message-redaction` now deletes from chat-history — an accepted channel `REDACT <chan> <msgid>` deletes the slice-274 stored line (`History::delete_by_msgid`, keyed `(target, msgid)`) so `CHATHISTORY` can never replay it; wired in both the local `CanSend::Ok` path and inbound `ENCAP * REDACT`, per-server; mirrors recording — a `+z` op-moderated/"shown to ops" attempt is never recorded so never deleted; authorization stays a stateless relay; 274 = IRCv3 `draft/chathistory` — SQLite-backed (`rusqlite`, bundled) channel message history behind an optional `ircd.kdl` `database {}` block; all channels auto-enrolled (PRIVMSG/NOTICE + JOIN/PART/QUIT recorded at the delivery plane), joining clients auto-replayed `replay-lines` recent lines unless they negotiated `draft/chathistory` (then exempt, per spec), full `CHATHISTORY LATEST/BEFORE/AFTER/BETWEEN/AROUND/TARGETS` (timestamp=/msgid= selectors) framed as a `chathistory` batch, `draft/chathistory`+`draft/event-playback` dynamic caps + `CHATHISTORY`/`MSGREFTYPES` ISUPPORT, age-based `retention-days` prune ticker; `Message` gained serde; 273 = registration-time nick `RESV` enforcement — a connection may no longer *register* directly as a reserved nick, only the post-registration `NICK`/JOIN were gated before; refuse `432` + reopen registration, mirroring the `433` nick-in-use path; 272 = charybdis `RESV`/`UNRESV` nick/channel reservation — JOIN→`437`/NICK→`432` gates, opers exempt, new `OperPrivilege::Resv`; defaults declared in `resv {}` config blocks, boot-seeded + REHASH-re-seeded keeping runtime `RESV`s). The slices span the full client command surface (registration; channels + the complete channel-mode surface; WHO/WHOIS/WHOX; OPER/STATS; and the query/util commands incl. OPERWALL/LOCOPS/USERIP); the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration); `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident); config live-rehash across every block; CertFP; the **IRCv3 cap track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track); elemental channel modes + extbans; KNOCK; command-rate flood protection (fakelag); the **complete charybdis channel-mode set** (`+C/+c/+S/+j/+z/+T/+g/+r/+f/+Q/+F/+P/+L`, with `+L` the last standard one) and **user-mode set** (`+D/+R/+Q/+G/+z/+S/+l`, plus the product decision that leveva has no local-only umodes — `SEND_UMODES` is every umode); and the `+s` server-notice **snomask** (a charybdis category mask with a producer wired for every category — the matrix is complete). Most const limits (`max-penalty`/`max-bans`/`service-string`/`default-snomask`/…) have been promoted to REHASH-able `options { … }` knobs. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. |
9494| **P12 — Retire the mechanical port; `leveva` is the product ✅ DONE (2026‑06‑13)** ([`docs/progress-log/p12.md`](docs/progress-log/p12.md)) | deleted `ircd-common`/`ircd-rs` + C‑era `iauth-rs` + `leveva-integration` + the mechanical port's deployment rigging; workspace = `leveva` + `leveva-iauth` | Ran the differential suite a **final time green** (`cargo test -p leveva-integration`, skeleton‑identical to the oracle), then **deleted the mechanical port** (`ircd-common`, `ircd-rs`), its C‑era auth scaffolding (`iauth-rs`, long since subsumed by the native `leveva-iauth`), and the oracle differential crate (`leveva-integration`). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/`use` dep on the deleted crates). Also stripped the mechanical port's **deployment rigging** (`docker/Dockerfile.ircd`, the `ircd` bake target, the entire `docs/k8s/` example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. | **MET. Final differential run green; `ircd-common`/`ircd-rs`/`iauth-rs`/`leveva-integration` deleted; `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` clean; `cargo test -p leveva -p leveva-iauth` green — the behavioral guarantees carry forward. The workspace is now `leveva` + `leveva-iauth`.** |
95959696---
···1597015970**Gate:** `cargo test -p leveva` (lib command::findforwards + channel forwards_to + findforwards_proptest
1597115971+ golden_findforwards + command::help; full lib suite 2643 pass) green; `cargo clippy -p leveva --tests`
1597215972clean; `cargo build --workspace` 0 warnings.
1597315973+1597415974+## P11 slice 292 — OPME (oper self-op of an opless channel), 2026-06-19
1597515975+1597615976+`OPME <channel>` — leveva's port of charybdis `extensions/m_opme.c`: an IRC operator grants
1597715977+**themselves** channel-operator (`+o`) status in a channel that currently has no operators (an
1597815978+*opless* channel). The recovery tool for a channel whose last op left, and the constrained,
1597915979+safety-gated sibling of `SAMODE +o` (which can force `+o` on anyone, anywhere): OPME only fires
1598015980+when the channel is genuinely opless and only ops the requester. Completes the charybdis oper
1598115981+channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb.
1598215982+1598315983+### Mechanism
1598415984+- **New atomic seam** `Channels::opme(name, uid) -> OpmeOutcome` (the fuzz target): under a single
1598515985+ `by_name` lock — channel missing → `NoSuchChannel`; `uid` not a member → `NotOnChannel`; any
1598615986+ member at op rank → `NotOpless`; else set the requester's `+o` bit and return `Opped { display }`.
1598715987+ Atomic so the opless check and the grant cannot race (a concurrent OPME sees the first grant and
1598815988+ is refused). No persistence hook — member status never persists (mirroring `Channels::deop`).
1598915989+- **`command/opme.rs`** gate ladder: not an operator (`+o`/`+O`) → `481` (mirrors `SAJOIN`/`KILL`);
1599015990+ no arg → `461`; then the seam routes `403`/`442`/the not-opless `NOTICE`/success. On `Opped`:
1599115991+ build `:<server> MODE <chan> +o <nick>`, fan to every local member (`deliver_uid`, skipping the
1599215992+ oper), relay server-sourced via `s2s::relay::server_channel_mode` (single `Member +o` change),
1599315993+ post a `+s` audit snomask `<oper> used OPME on <chan>`, and return the MODE echo to the oper.
1599415994+- Wired `"OPME" => opme::opme(...)` + `mod opme;` in `command/mod.rs`; `OpmeOutcome` added to the
1599515995+ `command` channel re-export. Help: `help/OPME.md` + `OPME` in the `help.rs` COMMANDS allowlist +
1599615996+ the operator line in `help/index.md`.
1599715997+1599815998+### Divergences (documented)
1599915999+- **leveva-native** — the C oracle is gone; IRCnet 2.11 has no OPME (charybdis extension). No
1600016000+ `leveva-integration` differential. The "is not opless"/audit wording is leveva's.
1600116001+- **Membership before opless** — charybdis checks opless *before* membership; leveva checks
1600216002+ membership first (`442` before the not-opless `NOTICE`) so a non-member oper never learns the
1600316003+ channel's op state (the FINDFORWARDS existence/membership precedent).
1600416004+- **`+s` audit snomask, not `+w` wallops** — charybdis announces OPME via `sendto_wallops_flags`;
1600516005+ leveva routes oper-override audit notices through the `+s` server-notice snomask, consistent with
1600616006+ the whole SA*/OJOIN family (slices 184/186).
1600716007+- **Local-only** — a read+grant of *this* server's channel table (charybdis's OPME is likewise
1600816008+ per-server); the grant still broadcasts to local members and relays to peers.
1600916009+1601016010+### Tests (TDD; inverse invariants + fuzzing)
1601116011+- `channel.rs` unit `opme_ops_only_when_opless`: opless member → `Opped` + now `is_op`; inverses —
1601216012+ a channel with an op → `NotOpless` (grants nothing), a non-member → `NotOnChannel`, a missing
1601316013+ channel → `NoSuchChannel`; fold-insensitive query; a second OPME (no longer opless) → `NotOpless`.
1601416014+- `command/opme.rs` units (4): `481` non-oper (channel untouched); `461`/`403`/`442` operand &
1601516015+ lookup errors; the not-opless `NOTICE` (oper still not opped); success ops the oper, returns the
1601616016+ `MODE #rust +o alice` echo, a co-member's mailbox sees the same server-sourced MODE, and the
1601716017+ inverse — a second OPME now returns the not-opless NOTICE.
1601816018+- `tests/opme_proptest.rs` (2 props): `opme_outcome_matches_channel_state` (model: random
1601916019+ base-member/requester membership + op presence + optional ghost target → exact outcome variant +
1602016020+ the op bit set iff already-op-or-`Opped`) and `arbitrary_args_never_panic` (replies ⊆
1602116021+ `{481, 461, 403, 442, NOTICE, MODE}`).
1602216022+- `tests/golden_opme.rs` (real binary, `dline.kdl` oper): non-oper bob `481`; alice deops herself
1602316023+ then `OPME #rust` → `:leveva.test MODE #rust +o alice` seen by alice **and** bob; a second OPME →
1602416024+ `NOTICE :Channel #rust is not opless`; `442` non-member; `403` nonexistent; `461` no-arg.
1602516025+1602616026+**Plan:** `docs/superpowers/plans/2026-06-19-p11-slice292-opme.md`.
1602716027+1602816028+**Gate:** `cargo test -p leveva` green (lib `command::opme` + `channel::opme` + `opme_proptest` +
1602916029+`golden_opme` + `command::help`; full lib suite 2648 pass); `cargo clippy -p leveva --tests` clean;
1603016030+`cargo build --workspace` 0 warnings.
···11+# P11 slice 292 — OPME (oper self-op of an opless channel)
22+33+## What
44+55+Port charybdis `extensions/m_opme.c` resolved to single-server leveva: `OPME <channel>` lets an
66+IRC operator grant **themselves** channel-operator (`+o`) status in a channel that currently has
77+**no** operators (an "opless" channel) — the standard recovery tool for a channel whose last op
88+left. The constrained, safety-gated sibling of `SAMODE +o` (which can force `+o` on anyone,
99+anywhere): OPME only works when the channel is genuinely opless and only ops the requester, so it
1010+is the *legitimate-recovery* path rather than a blanket override. Completes the charybdis oper
1111+channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb.
1212+1313+## Behaviour & gate ordering
1414+1515+1. Not an IRC operator (`+o`/`+O`) → `481 ERR_NOPRIVILEGES` (mirrors `SAJOIN`/`KILL`).
1616+2. No channel argument → `461 ERR_NEEDMOREPARAMS`.
1717+3. Channel does not exist → `403 ERR_NOSUCHCHANNEL`.
1818+4. Requester is not a member of the channel → `442 ERR_NOTONCHANNEL`.
1919+5. Channel already has at least one operator → `NOTICE <nick> :Channel <chan> is not opless`
2020+ (a NOTICE, charybdis-faithful — `m_opme` emits a notice, not a numeric; nothing changes).
2121+6. Success → grant the requester `+o`, broadcast `:<server> MODE <chan> +o <nick>` to every local
2222+ member (incl. an echo to the oper), relay the grant to the linked network server-sourced
2323+ (`s2s::relay::server_channel_mode`), and post a `+s` audit snomask
2424+ `<oper> used OPME on <chan>` (the SAMODE/SAJOIN audit-notice precedent).
2525+2626+**Divergence from charybdis order:** charybdis checks opless *before* membership; leveva checks
2727+membership first (442 before the not-opless NOTICE) so a non-member oper never learns the channel's
2828+op state — the FINDFORWARDS "existence/membership before the read" precedent.
2929+3030+## Divergences (documented)
3131+3232+- **leveva-native** — the C oracle is gone; IRCnet 2.11 has no OPME (charybdis extension). No
3333+ `leveva-integration` differential. The "is not opless" / audit-notice wording is leveva's.
3434+- **`+s` audit snomask, not `+w` wallops** — charybdis announces OPME via `sendto_wallops_flags`;
3535+ leveva routes oper-override audit notices through the `+s` server-notice snomask, consistent with
3636+ the whole SA*/OJOIN family (slices 184/186). Documented, deliberate.
3737+- **Local-only** — the grant broadcasts to local members and relays to peers, but OPME itself is a
3838+ read+grant of *this* server's channel table; there is no remote-channel routing (charybdis OPME
3939+ is likewise per-server).
4040+4141+## Mechanism / seams
4242+4343+- **New atomic seam** `Channels::opme(&self, name, uid) -> OpmeOutcome` (the fuzz target): under a
4444+ single `by_name` lock — channel missing → `NoSuchChannel`; `uid` not a member → `NotOnChannel`;
4545+ any member at op rank → `NotOpless`; else set the requester's `+o` bit and return
4646+ `Opped { display }`. Atomic so the opless-check and the grant cannot race. No persistence hook
4747+ (member status never persists, mirroring `deop`).
4848+- `command/opme.rs` — the gate ladder above; on `Opped`, build the server-prefixed MODE echo, fan
4949+ to local members (`deliver_uid`, skipping the oper), `server_channel_mode` relay with a single
5050+ `ModeChange::Member { add: true, mode: Op, uid }`, `snotice::server_notice`, and return the echo
5151+ to the oper. Local `notice()` helper (findforwards shape).
5252+- Wire-up: `mod opme;` + `"OPME" => opme::opme(client, msg, ctx)` in `command/mod.rs`; `help/OPME.md`
5353+ + `OPME` in the `help.rs` COMMANDS allowlist + the operator line in `help/index.md`.
5454+5555+## Tests (TDD — RED first; inverse invariants + fuzzing)
5656+5757+- `channel.rs` unit `opme_ops_only_when_opless`: opless member → `Opped` and now `is_op`; the
5858+ inverses — a channel with an op → `NotOpless` (requester stays non-op), a non-member → `NotOnChannel`,
5959+ a missing channel → `NoSuchChannel`; fold-insensitive channel arg; idempotence is irrelevant
6060+ (after success the channel is no longer opless, so a second OPME → `NotOpless`).
6161+- `command/opme.rs` units: `481` non-oper (channel untouched), `461` no-arg, `403` nonexistent,
6262+ `442` non-member, "not opless" NOTICE when an op exists (+ oper still not opped), success ops the
6363+ oper + emits the MODE echo + the inverse that a second OPME now returns the not-opless NOTICE.
6464+- `tests/opme_proptest.rs` (2 props): `opme_outcome_matches_channel_state` (model: random membership
6565+ + op presence → exact outcome variant and the op bit set iff `Opped`) and `arbitrary_args_never_panic`
6666+ (replies ⊆ `{481, 461, 403, 442, NOTICE, MODE}`).
6767+- `tests/golden_opme.rs` (real binary, oper config): non-oper `481`; oper OPME on an opless channel
6868+ → `:<server> MODE #chan +o oper` seen by the oper and a co-member; OPME on a channel with an op →
6969+ `NOTICE … is not opless`; `442` non-member; `403` nonexistent; `461` no-arg.
7070+7171+## Gate
7272+7373+`cargo test -p leveva` green (lib `command::opme` + `channel::opme` + `opme_proptest` +
7474+`golden_opme` + `command::help`); `cargo clippy -p leveva --tests` clean; `cargo build --workspace`
7575+0 warnings.
···11+OPME <channel>
22+33+Grants you channel-operator (+o) status in <channel> — but only if the channel
44+is "opless", meaning no member currently holds +o. It is the recovery tool for a
55+channel whose last operator has left: instead of force-setting a mode on anyone
66+anywhere (SAMODE), OPME only ops you, and only when the channel genuinely has no
77+operators.
88+99+You must be an IRC operator (otherwise 481) and you must already be a member of
1010+the channel (otherwise 442). The channel argument is required (without it you get
1111+461) and the channel must exist (otherwise 403). If the channel still has at
1212+least one operator you get a NOTICE saying it is not opless, and nothing changes.
1313+1414+On success the grant is broadcast to the channel as
1515+ :<server> MODE <channel> +o <you>
1616+seen by every member (including you) and relayed across the network. Operators
1717+watching the +s server-notice snomask see an audit line recording the OPME.
1818+1919+Examples:
2020+ OPME #help
2121+ OPME #abandoned
2222+2323+See also: MODE, SAMODE, OJOIN, CHANNEL_MODES.
···461461 Invited { display: String, stored: bool },
462462}
463463464464+/// The outcome of a [`Channels::opme`] attempt — `OPME <channel>` (P11 slice 292, charybdis
465465+/// `extensions/m_opme.c`): an operator self-ops in an *opless* channel.
466466+#[derive(Debug, Clone, PartialEq, Eq)]
467467+pub enum OpmeOutcome {
468468+ /// No channel by that (folded) name exists → `403 ERR_NOSUCHCHANNEL`.
469469+ NoSuchChannel,
470470+ /// The requester is not a member of the channel → `442 ERR_NOTONCHANNEL`. Checked before
471471+ /// the opless test so a non-member never learns the channel's op state.
472472+ NotOnChannel,
473473+ /// The channel already has at least one operator → a `NOTICE` (charybdis emits no numeric);
474474+ /// nothing is granted.
475475+ NotOpless,
476476+ /// The requester was a member of a genuinely opless channel and has been granted `+o`.
477477+ /// `display` is the canonical channel name.
478478+ Opped { display: String },
479479+}
480480+464481/// Why a JOIN was refused by [`Channels::check_join`] (each maps to a numeric).
465482#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466483pub enum JoinReject {
···17601777 .unwrap_or_default()
17611778 }
1762177917801780+ /// `OPME <name>` by `uid` (P11 slice 292, charybdis `extensions/m_opme.c`): grant `uid`
17811781+ /// channel-operator (`+o`) status **iff** the channel is opless — has no member at op rank.
17821782+ /// Atomic (single lock) so the opless check and the grant cannot race: a concurrent OPME
17831783+ /// sees the first grant and is refused [`OpmeOutcome::NotOpless`].
17841784+ ///
17851785+ /// Gate order — channel missing → [`OpmeOutcome::NoSuchChannel`]; `uid` not a member →
17861786+ /// [`OpmeOutcome::NotOnChannel`] (before the opless test, so a non-member never learns op
17871787+ /// state); any member already at op rank → [`OpmeOutcome::NotOpless`]; else set `uid`'s `+o`
17881788+ /// bit and return [`OpmeOutcome::Opped`]. No persistence hook — member status never persists
17891789+ /// (mirroring [`Channels::deop`]).
17901790+ pub fn opme(&self, name: &str, uid: &Uid) -> OpmeOutcome {
17911791+ let mut g = self.by_name.lock().expect("channels mutex poisoned");
17921792+ let Some(chan) = g.get_mut(&casemap::fold(name)) else {
17931793+ return OpmeOutcome::NoSuchChannel;
17941794+ };
17951795+ if !chan.members.contains_key(uid) {
17961796+ return OpmeOutcome::NotOnChannel;
17971797+ }
17981798+ if chan
17991799+ .members
18001800+ .values()
18011801+ .any(|s| s.rank() >= ChanMode::Op.rank())
18021802+ {
18031803+ return OpmeOutcome::NotOpless;
18041804+ }
18051805+ let status = chan.members.get_mut(uid).expect("membership checked above");
18061806+ *status = status.with(ChanMode::Op, true);
18071807+ OpmeOutcome::Opped {
18081808+ display: chan.name.clone(),
18091809+ }
18101810+ }
18111811+17631812 /// The ban masks on `name` (in add order), or `None` if no such channel.
17641813 pub fn bans(&self, name: &str) -> Option<Vec<String>> {
17651814 self.list_masks(name, ChanMode::Ban)
···27982847 set_fwd("#wheel", &bob(), Some("#somewhere"));
27992848 assert_eq!(ch.forwards_to("#hub"), vec!["#OTHER"]);
28002849 assert_eq!(ch.forwards_to("#somewhere"), vec!["#elsewhere", "#wheel"]);
28502850+ }
28512851+28522852+ /// `opme` (P11 slice 292) grants `+o` only when the channel is genuinely opless, and only to
28532853+ /// a member. The success case + every inverse (has-op, non-member, missing channel) and the
28542854+ /// fold-insensitive query.
28552855+ #[test]
28562856+ fn opme_ops_only_when_opless() {
28572857+ let ch = Channels::new();
28582858+ // #opless: alice creates it (chanop), then is deopped → no member at op rank.
28592859+ ch.join("#opless", &alice(), 0).unwrap();
28602860+ ch.join("#opless", &bob(), 0).unwrap(); // bob is a plain member
28612861+ assert!(ch.deop("#opless", &alice()));
28622862+ assert!(ch.op_members("#opless").is_empty(), "channel is now opless");
28632863+28642864+ // bob (a member of an opless channel) self-ops.
28652865+ match ch.opme("#OPLESS", &bob()) {
28662866+ // fold-insensitive query
28672867+ OpmeOutcome::Opped { display } => assert_eq!(display, "#opless"),
28682868+ other => panic!("expected Opped, got {other:?}"),
28692869+ }
28702870+ assert!(ch.is_op("#opless", &bob()), "bob now holds +o");
28712871+28722872+ // Inverse 1 — no longer opless: a second OPME (by alice, still a member) is refused.
28732873+ assert_eq!(ch.opme("#opless", &alice()), OpmeOutcome::NotOpless);
28742874+ assert!(!ch.is_op("#opless", &alice()), "the refused OPME granted nothing");
28752875+28762876+ // Inverse 2 — non-member: carol is not on #opless.
28772877+ assert_eq!(ch.opme("#opless", &carol()), OpmeOutcome::NotOnChannel);
28782878+28792879+ // Inverse 3 — missing channel.
28802880+ assert_eq!(ch.opme("#ghost", &alice()), OpmeOutcome::NoSuchChannel);
28012881 }
2802288228032883 /// `wipe_list_masks` (P11 slice 174) clears **all four** list modes — `+b`/`+e`/`+I`/`+R` —
···11+use crate::command::*;
22+use crate::UserMode;
33+44+/// `OPME <channel>` — leveva's port of charybdis `extensions/m_opme.c` (P11 slice 292). An IRC
55+/// operator grants **themselves** channel-operator (`+o`) status in a channel that currently has
66+/// no operators (an *opless* channel) — the recovery tool for a channel whose last op left. The
77+/// constrained, safety-gated sibling of [`SAMODE +o`](crate::command::mode::samode) (which can
88+/// force `+o` on anyone, anywhere): OPME only fires when the channel is genuinely opless and only
99+/// ops the requester, so it is the *legitimate-recovery* path. Completes the charybdis oper
1010+/// channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb.
1111+///
1212+/// Gate order:
1313+/// - Not an IRC operator (`+o`/`+O`) → `481 ERR_NOPRIVILEGES` (mirrors `SAJOIN`/`KILL`).
1414+/// - No channel argument → `461 ERR_NEEDMOREPARAMS`.
1515+/// - Channel does not exist → `403 ERR_NOSUCHCHANNEL`.
1616+/// - Requester is not a member → `442 ERR_NOTONCHANNEL`. (Checked **before** the opless test so a
1717+/// non-member never learns the channel's op state — the FINDFORWARDS existence/membership
1818+/// precedent; charybdis checks opless first.)
1919+/// - Channel already has an operator → `NOTICE <nick> :Channel <chan> is not opless` (charybdis
2020+/// emits a notice, not a numeric); nothing changes.
2121+/// - Success → grant `+o`, broadcast `:<server> MODE <chan> +o <nick>` to every local member (incl.
2222+/// an echo back to the oper), relay the grant to the linked network server-sourced
2323+/// ([`crate::s2s::relay::server_channel_mode`]), and post a `+s` audit snomask
2424+/// `<oper> used OPME on <chan>`.
2525+///
2626+/// Divergences (documented): leveva-native (no oracle — 2.11 has no OPME); the audit notice rides
2727+/// the `+s` snomask, not `+w` wallops (charybdis), matching the SA*/OJOIN family (slices 184/186);
2828+/// the "is not opless"/audit wording is leveva's; local-only (a read+grant of this server's
2929+/// channel table, like charybdis's per-server OPME).
3030+pub(crate) fn opme(client: &Registered, msg: &Message, ctx: &ServerContext) -> Vec<Message> {
3131+ if client.modes & (UserMode::Oper.bit() | UserMode::LocalOp.bit()) == 0 {
3232+ return vec![no_privileges(ctx, &client.nick)];
3333+ }
3434+ let Some(chan) = msg.params().first().filter(|s| !s.is_empty()) else {
3535+ return vec![need_more_params(ctx, &client.nick, "OPME")];
3636+ };
3737+3838+ match ctx.channels.opme(chan, &client.uid) {
3939+ OpmeOutcome::NoSuchChannel => vec![no_such_channel(ctx, &client.nick, chan)],
4040+ OpmeOutcome::NotOnChannel => vec![Message::builder(Numeric::ErrNotonchannel)
4141+ .prefix(&ctx.name)
4242+ .param(&client.nick)
4343+ .param(chan)
4444+ .trailing("You're not on that channel")
4545+ .build()],
4646+ OpmeOutcome::NotOpless => vec![notice(
4747+ ctx,
4848+ &client.nick,
4949+ format!("Channel {chan} is not opless"),
5050+ )],
5151+ OpmeOutcome::Opped { display } => {
5252+ // The granted change, rendered once for the local broadcast and the S2S relay.
5353+ let change = ModeChange::Member {
5454+ add: true,
5555+ mode: ChanMode::Op,
5656+ uid: client.uid.clone(),
5757+ };
5858+ let echo = Message::builder("MODE")
5959+ .prefix(&ctx.name)
6060+ .param(&display)
6161+ .param("+o")
6262+ .param(&client.nick)
6363+ .build();
6464+ let wire = echo.to_wire();
6565+ if let Some(members) = ctx.channels.members(&display) {
6666+ for m in &members {
6767+ if *m != client.uid && ctx.is_local_uid(m) {
6868+ ctx.registry.deliver_uid(m, wire.clone());
6969+ }
7070+ }
7171+ }
7272+ // Relay the grant to the linked network (server-sourced, UID param).
7373+ crate::s2s::relay::server_channel_mode(ctx, &display, std::slice::from_ref(&change));
7474+ // `+s` audit notice, mirroring the SAMODE/SAJOIN family.
7575+ crate::snotice::server_notice(ctx, &format!("{} used OPME on {display}", client.nick));
7676+ vec![echo]
7777+ }
7878+ }
7979+}
8080+8181+/// `:<server> NOTICE <nick> :<text>` — OPME's "not opless" refusal is a NOTICE, not a numeric
8282+/// (charybdis-faithful).
8383+fn notice(ctx: &ServerContext, nick: &str, text: impl Into<String>) -> Message {
8484+ Message::builder("NOTICE")
8585+ .prefix(&ctx.name)
8686+ .param(nick)
8787+ .trailing(text.into())
8888+ .build()
8989+}
9090+9191+#[cfg(test)]
9292+mod tests {
9393+ use super::*;
9494+ use crate::command::testutil::*;
9595+9696+ /// An opered `alice` connection (claimed in the registry) carrying `+o`.
9797+ fn opered(ctx: &ServerContext) -> Registered {
9898+ let _ = claim_observed(ctx, "alice");
9999+ let mut c = client();
100100+ c.modes |= UserMode::Oper.bit();
101101+ c
102102+ }
103103+104104+ fn drive(ctx: &ServerContext, c: &Registered, line: &str) -> Vec<Message> {
105105+ dispatch(&mut c.clone(), &Message::parse(line).unwrap(), ctx)
106106+ }
107107+108108+ /// Make `nick` a non-op member of `chan` (the channel must already exist).
109109+ fn join_plain(ctx: &ServerContext, nick: &str, chan: &str) {
110110+ let _ = claim_observed(ctx, nick);
111111+ ctx.channels.join(chan, &uid_for(nick), 0);
112112+ }
113113+114114+ /// The gate (and its inverse): a non-operator's OPME is `481` and grants nothing.
115115+ #[test]
116116+ fn opme_requires_oper() {
117117+ let ctx = ctx();
118118+ join_plain(&ctx, "alice", "#rust"); // alice creates #rust as chanop…
119119+ ctx.channels.deop("#rust", &uid_for("alice")); // …then is deopped → opless
120120+ let r = drive(&ctx, &client(), "OPME #rust");
121121+ assert_eq!(codes(&r), &["481"]);
122122+ assert!(
123123+ !ctx.channels.is_op("#rust", &uid_for("alice")),
124124+ "a gated OPME grants nothing"
125125+ );
126126+ }
127127+128128+ /// Operand / existence / membership errors land on the oper.
129129+ #[test]
130130+ fn opme_operand_and_lookup_errors() {
131131+ let ctx = ctx();
132132+ let oper = opered(&ctx);
133133+ assert_eq!(codes(&drive(&ctx, &oper, "OPME")), &["461"]);
134134+ assert_eq!(codes(&drive(&ctx, &oper, "OPME #ghost")), &["403"]);
135135+ // alice is an oper but not a member of bob's channel.
136136+ join_plain(&ctx, "bob", "#elsewhere");
137137+ ctx.channels.deop("#elsewhere", &uid_for("bob")); // opless, but alice isn't on it
138138+ assert_eq!(codes(&drive(&ctx, &oper, "OPME #elsewhere")), &["442"]);
139139+ }
140140+141141+ /// A channel that still has an operator → the "not opless" NOTICE, and nothing is granted.
142142+ #[test]
143143+ fn opme_refuses_a_channel_with_an_op() {
144144+ let ctx = ctx();
145145+ let oper = opered(&ctx);
146146+ // bob creates #rust (chanop) and stays opped; alice joins as a plain member.
147147+ join_plain(&ctx, "bob", "#rust");
148148+ ctx.channels.join("#rust", &uid_for("alice"), 0);
149149+ let r = drive(&ctx, &oper, "OPME #rust");
150150+ assert_eq!(codes(&r), &["NOTICE"]);
151151+ assert!(r[0].trailing().unwrap().contains("is not opless"));
152152+ assert!(
153153+ !ctx.channels.is_op("#rust", &uid_for("alice")),
154154+ "a refused OPME grants nothing"
155155+ );
156156+ }
157157+158158+ /// Success: the oper is opped, gets the MODE echo, and a co-member sees the broadcast.
159159+ #[test]
160160+ fn opme_ops_the_oper_in_an_opless_channel() {
161161+ let ctx = ctx();
162162+ let oper = opered(&ctx);
163163+ ctx.channels.join("#rust", &uid_for("alice"), 0); // alice (oper) creates it…
164164+ ctx.channels.deop("#rust", &uid_for("alice")); // …deopped → opless
165165+ let mut bob_rx = join_plain_rx(&ctx, "bob", "#rust"); // a co-member with a mailbox
166166+167167+ let r = drive(&ctx, &oper, "OPME #rust");
168168+ assert_eq!(codes(&r), &["MODE"]);
169169+ assert_eq!(r[0].command(), "MODE");
170170+ assert_eq!(r[0].params(), &["#rust", "+o", "alice"]);
171171+ assert_eq!(r[0].prefix(), Some(ctx.name.as_str()));
172172+ assert!(ctx.channels.is_op("#rust", &uid_for("alice")), "alice opped");
173173+174174+ // The co-member sees the same server-sourced MODE.
175175+ let seen = delivered(&mut bob_rx);
176176+ assert_eq!(seen.command(), "MODE");
177177+ assert_eq!(seen.params(), &["#rust", "+o", "alice"]);
178178+179179+ // Inverse: a second OPME is now refused (the channel is no longer opless).
180180+ let r2 = drive(&ctx, &oper, "OPME #rust");
181181+ assert_eq!(codes(&r2), &["NOTICE"]);
182182+ assert!(r2[0].trailing().unwrap().contains("is not opless"));
183183+ }
184184+185185+ /// Like [`join_plain`] but returns the new member's mailbox receiver.
186186+ fn join_plain_rx(
187187+ ctx: &ServerContext,
188188+ nick: &str,
189189+ chan: &str,
190190+ ) -> tokio::sync::mpsc::UnboundedReceiver<crate::registry::Envelope> {
191191+ let rx = claim_observed(ctx, nick);
192192+ ctx.channels.join(chan, &uid_for(nick), 0);
193193+ rx
194194+ }
195195+}
···11+//! Property-based fuzzing of `OPME` (slice 292) — operator self-op of an opless channel.
22+//!
33+//! Two surfaces:
44+//!
55+//! - **`opme_outcome_matches_channel_state`** — the pure atomic seam [`Channels::opme`]. For a
66+//! randomly-constructed channel (a base member that may or may not be opped; the requester that
77+//! may or may not be a member, may or may not already be opped) and an optionally-nonexistent
88+//! target, the returned outcome must match the model — `NoSuchChannel` / `NotOnChannel` /
99+//! `NotOpless` / `Opped` — and the requester ends up opped **iff** it was already opped or the
1010+//! call returned `Opped`. The conservation invariant: OPME grants `+o` only on a genuinely
1111+//! opless channel the requester belongs to, and never otherwise.
1212+//! - **`arbitrary_args_never_panic`** — `OPME` at the [`dispatch`] layer with arbitrary printable
1313+//! args never panics and only ever replies `{481, 461, 403, 442, NOTICE, MODE}`.
1414+1515+use std::sync::Arc;
1616+1717+use leveva::channel::{Channels, ModeChange, OpmeOutcome};
1818+use leveva::command::{dispatch, Registered};
1919+use leveva::config::Admin;
2020+use leveva::mode::ChanMode;
2121+use leveva::registry::Registry;
2222+use leveva::server::{Counters, ServerContext};
2323+use leveva::{Message, Sid, Uid};
2424+use proptest::prelude::*;
2525+2626+const SERVER: &str = "leveva.test";
2727+2828+fn ctx() -> Arc<ServerContext> {
2929+ Arc::new(ServerContext {
3030+ name: SERVER.to_string(),
3131+ description: "T".to_string(),
3232+ network: "TestNet".to_string(),
3333+ version: "leveva-0.0.0",
3434+ created: "FIXED".to_string(),
3535+ motd: None,
3636+ counters: Counters::default(),
3737+ registry: Registry::new(),
3838+ channels: Channels::new(),
3939+ whowas: leveva::whowas::WhowasHistory::default(),
4040+ uids: leveva::uid::UidGenerator::new(Sid::try_from("0ABC").unwrap()),
4141+ default_user_modes: 0,
4242+ default_channel_modes: 0,
4343+ operators: Vec::new(),
4444+ stats_conf: leveva::server::StatsConf::default(),
4545+ klines: leveva::kline::KlineStore::new(),
4646+ dlines: leveva::dline::DlineStore::new(),
4747+ xlines: leveva::xline::XlineStore::new(),
4848+ net: leveva::s2s::Network::new(),
4949+ peers: leveva::s2s::PeerLinks::new(),
5050+ monitors: leveva::monitor::Monitors::new(),
5151+ conn_limits: leveva::connlimit::ConnLimits::new(),
5252+ linking: leveva::link::LinkingSet::new(),
5353+ knock_throttle: leveva::knock_throttle::KnockThrottle::new(),
5454+ knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(),
5555+ resvs: leveva::resv::ResvStore::new(),
5656+ history: leveva::history::History::disabled(),
5757+ auth: leveva::server::AuthConfig::default(),
5858+ admin: Admin::default(),
5959+ })
6060+}
6161+6262+fn uid(i: usize) -> Uid {
6363+ let c = (b'A' + i as u8) as char;
6464+ Uid::try_from(format!("0ABCAAAA{c}").as_str()).unwrap()
6565+}
6666+6767+/// The requester — an operator, so the `481` gate never masks the outcome path at dispatch.
6868+fn oper() -> Registered {
6969+ Registered {
7070+ uid: uid(0),
7171+ nick: "alice".into(),
7272+ user: "u".into(),
7373+ host: "h".into(),
7474+ realname: "R".into(),
7575+ modes: leveva::UserMode::Oper.bit(),
7676+ privileges: Vec::new(),
7777+ caps: Default::default(),
7878+ }
7979+}
8080+8181+/// Set or clear `uid`'s `+o` on `chan` (the member must already exist).
8282+fn set_op(ctx: &ServerContext, chan: &str, who: Uid, on: bool) {
8383+ if on {
8484+ ctx.channels.apply_modes_as_server(
8585+ chan,
8686+ &[ModeChange::Member {
8787+ add: true,
8888+ mode: ChanMode::Op,
8989+ uid: who,
9090+ }],
9191+ );
9292+ } else {
9393+ ctx.channels.deop(chan, &who);
9494+ }
9595+}
9696+9797+proptest! {
9898+ /// The seam's outcome is exactly what the channel state dictates, and the `+o` grant happens
9999+ /// only on the `Opped` arm.
100100+ #[test]
101101+ fn opme_outcome_matches_channel_state(
102102+ base_opped in any::<bool>(),
103103+ requester_member in any::<bool>(),
104104+ requester_opped in any::<bool>(),
105105+ query_ghost in any::<bool>(),
106106+ ) {
107107+ let ctx = ctx();
108108+ // A base member keeps the channel alive; it auto-ops on creation, so its op state is then
109109+ // set explicitly. uid(99) maps to a distinct fixture UID.
110110+ ctx.channels.join("#c", &uid(25), 0);
111111+ set_op(&ctx, "#c", uid(25), base_opped);
112112+ if requester_member {
113113+ ctx.channels.join("#c", &uid(0), 0); // non-op (channel already exists)
114114+ set_op(&ctx, "#c", uid(0), requester_opped);
115115+ }
116116+117117+ let target = if query_ghost { "#ghost" } else { "#c" };
118118+ let outcome = ctx.channels.opme(target, &uid(0));
119119+120120+ let expected = if query_ghost {
121121+ OpmeOutcome::NoSuchChannel
122122+ } else if !requester_member {
123123+ OpmeOutcome::NotOnChannel
124124+ } else if base_opped || requester_opped {
125125+ OpmeOutcome::NotOpless
126126+ } else {
127127+ OpmeOutcome::Opped { display: "#c".to_string() }
128128+ };
129129+ prop_assert_eq!(&outcome, &expected, "state→outcome mismatch");
130130+131131+ // The requester holds +o afterwards iff it was already opped or the call just opped it.
132132+ let opped_now = ctx.channels.is_op("#c", &uid(0));
133133+ let should_be_op = requester_member
134134+ && (requester_opped || matches!(outcome, OpmeOutcome::Opped { .. }));
135135+ prop_assert_eq!(opped_now, should_be_op, "op-bit conservation");
136136+ }
137137+138138+ /// Arbitrary args never panic; the reply is always in the allowed set.
139139+ #[test]
140140+ fn arbitrary_args_never_panic(args in "[#a-zA-Z0-9 ,:*!@._-]{0,40}") {
141141+ let ctx = ctx();
142142+ // An opless channel the oper belongs to, so the success (MODE) path is reachable for the
143143+ // matching query.
144144+ ctx.channels.join("#c", &uid(0), 0);
145145+ ctx.channels.deop("#c", &uid(0));
146146+147147+ let line = format!("OPME {args}");
148148+ let Ok(msg) = Message::parse(&line) else { return Ok(()); };
149149+ let r = dispatch(&mut oper(), &msg, &ctx);
150150+ const ALLOWED: &[&str] = &["481", "461", "403", "442", "NOTICE", "MODE"];
151151+ for c in r.iter().map(|m| m.command()) {
152152+ prop_assert!(ALLOWED.contains(&c), "unexpected reply {c} for {line:?}");
153153+ }
154154+ }
155155+}
···11+---
22+source: leveva/tests/golden_opme.rs
33+expression: transcript
44+---
55+bob 481: :leveva.test 481 bob :Permission Denied- You're not an IRC operator
66+alice sees MODE: :leveva.test MODE #rust +o alice
77+bob sees MODE: :leveva.test MODE #rust +o alice
88+not opless: :leveva.test NOTICE alice :Channel #rust is not opless
99+alice 442: :leveva.test 442 alice #bobland :You're not on that channel
1010+alice 403: :leveva.test 403 alice #ghost :No such channel
1111+alice 461: :leveva.test 461 alice OPME :Not enough parameters