docs(plan): design doc for C architecture
Plan for docs/design.md — reference guide to ircd.rs C daemon
architecture (boot, event loop, command dispatch, I/O, send, users,
channels, S2S, data structures, config, DNS, iauth, build).
Scope: 15 subsystems, ~300 lines, verified symbols, C-only.
Assisted-by: Claude Haiku 4.5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
feat(ircd-common): port channel.c m_kick handler + find_chasing to Rust (P5bb)
Ninth channel.c sub-phase, via the existing channel_link.o partial port
(-DPORT_CHANNEL_P5). m_kick (the KICK command) + the file-static find_chasing
(history-aware nick resolver) ported to ircd-common/src/channel.rs.
find_chasing is the cluster's shared static: m_kick is its caller, but the
still-C set_mode/m_mode also call it, so its C body is guarded out and an extern
forward decl added under the guard (it had none) so the C remnant resolves to the
Rust def at link (P5u/P5x extern-prototype switch, linkage-only). check_string
(the other display static) is untouched — m_kick does not call it.
Every other callee is already Rust (get_channel [extern-switched by P5aa],
check_channelmask, is_chan_op, remove_user_from_channel, find_uid, find_client,
get_history, mystrdup) or a C variadic sender (sendto_flag/sendto_match_servs_v
added to the extern block). nbuf is a local auto array (not a file-static); the C
size_t maxlen/clen arithmetic and the find_uid/find_chasing short-circuit
assignment are mirrored branch-for-branch. The SV_UID server relay carries UIDs.
L2/L2-S2S are the gates (no L1: find_chasing is file-static so no cref_ oracle
symbol; m_kick has no extractable data-op core).
feat(ircd-common): port channel.c mode-id list layer to Rust (P5ff)
Port the ban/exception/invite mode-id list-item layer — the connected
component over the file-static asterix var: check_string, make_bei,
free_bei, add_modeid, del_modeid (channel.c:176/212/193/263/331).
All five are file-static in C and still called by the C set_mode (and
free_channel), so each gets the P5u/P5x extern-prototype switch: the
static forward decl flips to extern under -DPORT_CHANNEL_P5 and the body
is guarded out, resolving the C remnant to the Rust #[no_mangle] def at
link. check_string had no forward decl (defined before its callers) → a
new extern decl added under the guard.
asterix becomes module-private (static mut ASTERIX): C never references
it outside the now-ported functions, so the Rust pointer identity is
self-consistent (make_bei stores it, free_bei compares it, C only passes
the resulting aListItem). istat.is_bans/is_banmem use wrapping_add/sub
(u_long, cf. P5u add_invite). collapse/mycmp/make_link/free_link/istat
already Rust; MyMalloc/free/strncpy/isspace via libc.
No L1 (file-statics → no cref_ oracle, cf. P5v/P5dd); L2 byte-identity
through the still-C set_mode is the faithfulness gate.
Plan: docs/superpowers/plans/2026-06-03-p5ff-channel-modeid-list.md
feat(ircd-common): port channel.c mode core to Rust (P5gg)
Port the last large channel.c cluster — m_mode, set_mode, send_mode_list,
send_channel_modes, send_channel_members — to ircd-common/src/channel.rs,
extending the channel_link.o partial port (-DPORT_CHANNEL_P5).
This is the connected component over the shared file-static scratch buffers
modebuf/parabuf/uparabuf. m_njoin (the only other user) is already Rust
(P5ee), so after this port those C statics have zero C users and become
module-private (MODEBUF/PARABUF reused, UPARABUF added).
All five take a plain #ifndef guard:
- set_mode/send_mode_list are file-static with no remaining C caller (their
callers m_mode/send_channel_modes port here) -> private Rust unsafe fns.
- send_channel_modes/send_channel_members are exported and still called by
the C send_server_burst -> #[no_mangle] resolves s_serv's calls at link.
- m_mode's decl resolves via the parse.rs bindings import.
Faithful port of the full mode grammar (+/- o/v/O/k/b/e/I/R/l + the simple
flag table, the opcnt chops reconstruction, parseNUH via the Rust make_bei,
the k key sanitizer, the reop scheduling) and the SV_UID/sendto_channel_butserv
output cascade.
Only the channel lifecycle group (reop_channel/collect_channel_garbage/
setup_server_channels/free_channel/get_channel) remains C.
feat(ircd-common): port s_user.c is_allowed (P5o)
Port the per-client ACL gate `is_allowed` (s_user.c:3528) to
ircd-common/src/s_user.rs, extending the s_user_link.o partial port
(new -DPORT_USER_P5 guard around s_user.c:3523-3564). It is a pure
function of `cptr` (reads fd/status/service->wants/the confs chain; no
globals, no hash), which makes it cleanly L1-differentiable.
Callers resolve to the Rust def: parse.rs (penalty checks) switches to
`use crate::s_user::is_allowed`, and the still-C callers (m_kill,
s_serv, s_conf, channel, res, hash, s_bsd) bind at link once s_user.o's
is_allowed is dropped (the canonize/send_away pattern).
Verification:
- L1 s_user_is_allowed_diff: zero-diff vs cref_is_allowed over 11 cases
(server, non-MyConnect, service wants-{tkline,kline,none}, no-conf,
non-oper-conf, CONF_OPERATOR grant-all/some/none, oper-second-in-chain).
- L2 golden_s_user_is_allowed: byte-identical grant/deny branch selection
via oper KILL — kill_grant.conf (K flag -> is_allowed=1 -> 401) and
kill_deny.conf (T flag, no ACL_KILL -> is_allowed=0 -> m_nopriv 481).
Only an oper reaches m_kill (KILL col 1 = m_nopriv); O-line flags are
field 7 (after port+class). OPER_KILL is defined so K survives.
- is_allowed defined T from ircd-common; full cargo test green; 0 warnings.
is_allowed emits no wire output, so format! is N/A. next_client/
hunt_server (hash-dependent) and the NICK/registration cluster stay C.
feat(ircd-common): port s_user.c m_user (P5r)
Port the USER registration handler from C to Rust, extending the
existing s_user_link.o partial port with one more #ifndef PORT_USER_P5
guard region (s_user.c:2337-2470). m_user now resolves to the Rust def
at link; the cref oracle keeps the full unguarded s_user.o.
m_user is a handler cluster (msgtab USER row reaches it only on the
unregistered column). It allocates the anUser, parses the +/- and
RFC-bit umodes carried in USER, caps the real-name info, and tail-calls
the already-Rust register_user (P5q). No file-statics of its own, so the
C remnant's shared buf/buf2/user_modes are untouched.
Config-resolved: DEFAULT_INVISIBLE off (no SetInvisible), XLINE off (no
user2/user3), REALLEN=50, USERLEN=10. aConfItem.flags is c_long so the
PFLAG_SERVERONLY/PFLAG_TLS checks cast accordingly.
format! is N/A here: every string op copies arbitrary client-supplied
C-string bytes under byte-precision truncation (direct libc copies), and
both wire emissions go straight to the C variadic senders.
L2 gate: golden_registration byte-identical (its NICK/USER line drives
make_user + the "no flags" umode path + the register_user tail-call).
Full cargo test green (40 binaries, 0 failed); cargo build 0 warnings.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
feat(ircd-common): port s_user.c NICK cluster (P5s)
Port m_nick, m_unick, the file-static save_user (SAVE emitter), and m_save
into ircd-common/src/s_user.rs, extending the s_user_link.o partial port
(two new PORT_USER_P5 guards). These form one connected component: all three
handlers call save_user. Only next_client/hunt_server (hash-dependent, S2S/L2
deferred) now remain in s_user_link.o.
m_nick's `goto badparamcountkills`/`goto nickkilldone` control flow is
refactored into two faithful helper fns (bad_param_count_kills, nick_kill_done)
so each goto becomes `return X(...)`. Path-string assembly uses byte-builders
into a private NICK_PATH_BUF (arbitrary C-string bytes can't pass through a
UTF-8 String); ASCII/numeric formatting stays format!-eligible. Config
resolved: USE_SERVICES/CLIENTS_CHANNEL off, DISABLE_NICK0/NICKCHANGE absent.
L2 golden_s_user_nick (local nick change, 432 erroneous nick, 433 nick-in-use)
byte-identical vs reference-C. m_unick/m_save/save_user SAVE emit are S2S-only
(no L2 path under -s), ported faithfully, covered by review. Full cargo test
green; 0 warnings.
feat(ircd-common): port channel.c m_topic handler to Rust (P5y)
Port m_topic (the TOPIC command) to ircd-common/src/channel.rs, extending the
channel_link.o partial port (-DPORT_CHANNEL_P5). The cleanest channel.c handler:
every callee is already Rust (canonize/strtoken/find_channel/check_channelmask/
is_chan_op) or a C variadic sender, and m_topic has no file-static of its own
(it uses find_channel, not get_channel), so a plain #ifndef guard drops the C
def and the parse.rs bindings import resolves to the Rust def at link — no
extern-prototype switch, no parse.rs/build.rs edit.
TOPIC_WHO_TIME is defined, so the topic_t/topic_nuh blocks are ported: the 333
RPL_TOPIC_WHO_TIME reply, and on set the topic_nuh "name!user@host" build (a
byte-builder, not sprintf/format!, per the standing C-string-bytes rule) +
topic_t = timeofday. USE_SERVICES off, so the check_services_butone block is
omitted. The 333 topic_t passed to %lu is cast to u_long (the P5c width rule).
Add the 333 RPL_TOPIC_WHO_TIME canonicalizer mask (the trailing topic_t is
wall-clock volatile between the separately-booted ref-C/Rust binaries).
Builds with channel_link.o substituted (m_topic resolves T from ircd-common);
0 warnings.
port(p5bbb): m_server/m_server_estab/m_smask + burst cluster C → Rust; drop s_serv.o
Final s_serv.c cluster: the server-introduction / link-establishment / burst
core — m_server, m_server_estab, m_smask, check_version, get_version,
send_server, introduce_server, send_server_burst + the check_servername_errors[]
table. With every s_serv.c symbol now in ircd-common, s_serv.o is dropped
outright (added to PORTED; the P5ii..P5aaa s_serv_link.o second-compile +
-DPORT_SERV_P5 removed; the cref oracle keeps the unguarded s_serv.o). s_serv.c
is now fully Rust.
Faithful, config-resolved: USE_SERVICES/ZIP_LINKS/JAPANESE/CRYPT_LINK_PASSWORD/
HUB/USE_SYSLOG all off (those arms not compiled; encr=cptr->passwd; the
#ifndef HUB leaf-check IS compiled). check_servername_errors is now a
#[no_mangle] pub static mut (raw-pointer table, the replies[] pattern) since the
C BSS def is gone and ircd.c:1056 reads it.
feat(p5rr): port s_serv.c m_close/m_rehash oper-maintenance cluster to Rust
Tenth s_serv.c sub-phase. m_close (CLOSE — drop pending/unknown connections)
and m_rehash (REHASH — reload config) ported into ircd-common/src/s_serv.rs,
extending the s_serv_link.o partial port (-DPORT_SERV_P5). Both are leaf
handlers with no file-static of their own → plain #ifndef guards drop the C
defs and the parse.rs msgtab decls resolve to the Rust defs at link.
Config-resolved: DELAY_CLOSE on → m_close's is_delayclosewait/delay_close(-2)
tail block ported; USE_SYSLOG off → m_rehash's syslog line not compiled/ported.
Faithful: the for(i=highest_fd;i;i--) loop never touches local[0]; RPL_CLOSING
carries acptr->status as the %d arg; exit_client(.., &me, ..) closes each fd;
m_rehash returns rehash(cptr,sptr, parc>1 ? parv[1][0] : 0).
Callees all resolve: is_allowed (P5o), m_nopriv (P4), exit_client (P5c),
rehash (C extern s_conf.c), mybasename+configfile (P1/global), get_client_name
/delay_close (C externs), sendto_one/sendto_flag (C variadic), local[] via
local_base(), highest_fd/me/istat globals. New is_connecting/is_handshake
inline helpers. cargo build 0 warnings; nm confirms m_close/m_rehash from Rust.
port(p5zz): m_die + m_restart (s_serv.c) oper-shutdown cluster → Rust
The oper-shutdown leaf pair, the analogue of P5rr's m_close/m_rehash. Both
are clean independent leaves under the existing s_serv_link.o partial port
(-DPORT_SERV_P5): plain #ifndef guards, no extern-prototype switch, no
parse.rs/build.rs edit (their msgtab decls resolve to the Rust defs at link).
m_die uses only a function-local killer[] stack buffer. m_restart's C
sprintf(buf, ...) wrote the file-static buf purely as in-call scratch (passed
straight to restart()); the still-C check_version/m_server_estab keep that
static, so the Rust port uses a module-private RESTARTBUF — behaviorally
identical. Faithful: the local[] fd-loop, the per-client/service Server
{Terminating,Restarting} notice + EXITC_DIE + exit_client, the IsServer ERROR
line, flush_connections(me.fd), then the terminal s_die(0)/restart(buf).
Both re-gate on is_allowed(ACL_DIE/ACL_RESTART) → m_nopriv/481 for a flag-less
oper. The notice loop + terminal s_die/restart are destructive (review-covered);
no IsServer(cptr) dispatch branch or remote-user fields → no S2S.
port(p6a): m_tkline/m_untkline cluster → Rust; s_conf_link.o partial port
First P6 sub-phase. Port the s_conf.c TKLINE cluster (wdhms2sec, do_kline,
prep_kline, m_tkline, m_untkline, tkline_expire + the tkconf list global) to
ircd-common/s_conf.rs. config_read.c is #include-d into s_conf.c (one big P6
TU), so this is a partial port: #ifndef PORT_TKLINE_P6 guards in s_conf.c +
an s_conf_link.o second-compile (carrying the same machine-path defines as the
native s_conf.o recipe) substituted into the link set. The cref oracle keeps
the full unguarded s_conf.o.
tkconf was a file-static; surviving C (find_kill) still references it, so the
guard swaps its definition for an extern decl and Rust owns the definition.
make_conf/free_conf (list.rs), find_class (class.rs), match/exit_client/
is_allowed/m_nopriv are already Rust; kconf/match_ipmask/ipv6_convert/
get_client_name/nexttkexpire stay C externs.
Config-resolved: KLINE off (m_kline + the kline-file-write block not
compiled/ported), TKLINE on, TKLINE_MAXTIME=99999999, SCH_TKILL=SCH_NOTICE.
feat(p6i): port s_conf.c K-line lookup family to Rust
Port the four read-only K-line / version-mask / deny lookups named in the
PLAN P6 row — find_kill, find_two_masks, find_conf_flags, find_denied — to
ircd-common/src/s_conf.rs via the existing s_conf_link.o partial port
(-DPORT_SCONF_FINDKILL_P6, three guard regions; find_bounce stays C).
Config-resolved: TKLINE on (find_kill's tkconf→kconf two-pass loop compiles),
KLINE off, TIMEDKLINES off (the reply/now/check_time_interval block is not
compiled; the timedklines arg is inert). Faithful translation of the
match/match_ipmask host/CIDR matching, the '='-numeric-only skip, the
resolved-vs-unresolved branches, the CONF_TKILL/CONF_KILL username-vs-ident
`check` selection, and find_denied's isdigit-passwd class cross-check +
'!'-reversed svrtop host scan.
Reuses the existing match_/match_ipmask/mycmp/bad_ptr/reply/me_name helpers;
adds inetntop/ipv6string/find_client/svrtop/aServer imports + strncasecmp/
strpbrk to the libc extern block. The still-C callers (s_serv.c check_version,
s_user.c register_user, ircd.c) resolve to the Rust defs at link.
feat(p6k): port s_conf.c attach/detach mutating cluster to Rust
Port the connected component of conf-attachment mutators — attach_conf,
detach_conf, det_confs_butmask, attach_confs, attach_confs_host (+ the
private is_attached/add_cidr_limit/remove_cidr_limit) — to
ircd-common/src/s_conf.rs via the s_conf_link.o partial port
(-DPORT_SCONF_ATTACH_P6, three guard regions). These grow/shrink a local
client's cptr->confs Link chain + the global conf list, mutate class
refcounts (aconf->clients / ConfLinks) and the per-class CIDR-limit
patricia trees.
Config-resolved: ENABLE_CIDR_LIMITS on (add/remove_cidr_limit compile),
YLINE_LIMITS_IPHASH on (the per-host limit loop walks the IP hash),
YLINE_LIMITS_OLD_BEHAVIOUR off (max-links gate is ConfLinks >= ConfMaxLinks).
pnode->data is a void* used as an int counter (void-ptr arith = +1 byte).
What stays C in s_conf.c is now only the rest of the mutating surface:
attach_Iline + count_cnlines (callers of this core, separate P6l bite),
rehash, and the initconf-private lookup_confhost/ipv6_convert.
feat(p6p): port res_init.c (resolver-state init), drop res_init.o
Port ircd/res_init.c to ircd-common/res_init.rs — the BIND-derived resolver
bootstrap: ircd_res_init() (reads /etc/resolv.conf + LOCALDOMAIN/RES_OPTIONS
into the global ircd_res), the static res_setoptions it drives, and the random
message-id generator ircd_res_randomid(). The ircd_res global moves from C to a
Rust #[no_mangle] static mut; res_mkquery.rs / res.c / s_bsd.c resolve their
ircd_res / ircd_res_init references to the Rust defs at link.
res_init.o added to PORTED and dropped outright (no _link.o) — self-contained
leaf, the only non-libc dep is inetpton (P1 support.rs). Config-resolved: DEBUG /
NEXT / RESOLVSORT / RFC1535 / HAVE_GETIPNODEBYNAME / USELOOPBACK / __BIND_RES_TEXT
all OFF, so only the simple path compiles (no NetInfo, no sortlist, no debug
printfs, default-dnsrch derivation active, inet6 -> RES_USE_INET6). After this,
res.c (the resolver proper) is the only DNS file still in C.
feat(p6t): port res.c cache build/insert half (add_to_cache/make_cache)
The fourth and final bite of the res.c hostent-cache cluster, built on the
P6q hashes + P6r removal core + P6s lookup/reorder half. With this res.c's
cache is fully Rust; only the resolver proper (request queue + answer parser
+ socket I/O) remains C.
add_to_cache: list-head push + both hash-bucket links + ++incache>MAXCACHED
LRU tail-evict via rem_cache + ca_adds++. make_cache: dedup-or-build (T_PTR
-> find_cache_number per reply addr / T_A,T_AAAA -> find_cache_name by name;
a hit returns the already-reordered+merged entry, a miss MyMallocs a fresh
entry, builds the contiguous IP block + pointer array, steals the ResRQ
aliases + h_name (source nulled), clamps ttl<600->600 + re_shortttl++, sets
expireat + the FLG_*_VALID flag, then add_to_cache).
Seam: res_link.o partial port (-DPORT_RES_CACHE_BUILD_P6t); both functions
flip static->RES_CACHE_LINKAGE so the still-C proc_answer caller resolves to
the Rust make_cache, and -DRES_CACHE_EXPOSE de-statics them in the cref
oracle for L1. Config: DEBUG off, AFINET=AF_INET6 (16-byte IN_ADDR),
MAXCACHED 512.
feat(p7b): port common/packet.c (dopacket) to Rust — packet.o dropped
The inbound packet reassembler. Single exported symbol; ZIP_LINKS/DEBUGMODE
off so only the plain while(length>0 && ch2) line-reassembly loop compiles.
All callees already Rust (parse, exit_client, me, IsDead/IsServer) → packet.o
dropped outright (no _link.o).
L1 differential (ircd-testkit/tests/packet_diff.rs), 8 cases vs cref_dopacket,
isolated me/cref_me worlds, full post-state (retval, count, 512-byte buffer,
receiveB/receiveM on cptr/acpt/me): single LF, single CR, skipped extra CR-LF,
two-lines-one-buffer, partial->continuation round-trip, acpt==&me guard,
distinct acpt, 600-byte overflow truncation. Zero-diff. Serialized on a
GLOBALS_LOCK (shared by-value globals; the P7 L1 race).
L2: existing golden suite covers the universal read path (golden_registration
byte-identical). Utility TU, no IsServer-remote-field formatting -> no S2S test.
feat(p7c): port ircd.c tune-file pair (ircd_writetune/readtune) to Rust
Third P7 step; first (partial) port of the central driver ircd.c, leaf-first.
ircd_writetune/ircd_readtune persist the 7 runtime sizing globals (whowas/lock
sizes, the 4 hash sizes, poolsize) to the tune file: sprintf one-decimal-per-line
on write, sscanf back on boot, with the USE_HOSTHASH/USE_IPHASH mirrors
(_HOSTNAMEHASHSIZE/_IPHASHSIZE = _HASHSIZE, both ON), the BOOT_BADTUNE
silent-return-vs-exit(1) branch, and the lk_size = max(lk_size, ww_size) clamp.
Partial port via ircd_link.o (-DPORT_IRCD_TUNE_P7c) driven through make so the
recursive IRCD*_PATH/IAUTH path defines the surviving C needs stay byte-identical;
c_ircd_main, the me global, signals, restart/server_reboot and the tune callers
stay C. cref oracle keeps the unguarded ircd.o so cref_ircd_writetune/readtune
survive for L1. 0 warnings; golden boot path (ircd_readtune at startup) stays
byte-identical.
feat(p7d): port s_bsd.c socket/file leaves (get_sockerr/set_non_blocking/write_pidfile)
First (partial) port of the central I/O TU s_bsd.c. Port the three pure
libc-wrapper leaves that touch none of the event-loop data globals
(local[]/highest_fd/timeofday/FdAry) to ircd-common/src/s_bsd.rs:
- get_sockerr: getsockopt(SO_ERROR) when fd>=0, else the saved errno.
- set_non_blocking: fcntl(F_GETFL)->fcntl(F_SETFL|O_NONBLOCK); the failure
arms call the still-C report_error (an output fn, L2-covered).
- write_pidfile: truncate+create IRCDPID_PATH, sprintf("%5d\n", getpid()),
write, close (libc verbatim so the bytes are byte-identical).
Mechanism = the standard <tu>_link.o second-compile: map s_bsd.o ->
s_bsd_link.o in build.rs::link_objs(), compiled with -DPORT_S_BSD_LEAF_P7d
(carrying s_bsd.o's IRCDPID_PATH/IAUTH_PATH/IAUTH machine-path defines so the
surviving C remnant is byte-identical). The three C bodies are wrapped in
#ifndef PORT_S_BSD_LEAF_P7d in ircd/s_bsd.c. Everything else (the event loop,
listeners, add_connection/read_message, report_error, get_my_name,
add_local_domain, start_iauth) stays C.
IRCDPID_PATH is an s_bsd.o command-line define (absent from bindgen); expose
it to Rust via the P6m make-`--eval`+rustc-env pattern -> ircd_sys::PID_PATH.
RED confirmed exactly get_sockerr/set_non_blocking/write_pidfile undefined
(referenced by ircd.c, s_auth.c, and s_bsd.c open_listener/start_iauth/
read_message); after GREEN, nm target/debug/ircd shows all three as T (Rust).
Workspace builds 0 warnings; golden_registration byte-identical.
feat(p7bb): port s_bsd.c add_connection + check_clones (inbound acceptor)
Port the inbound-connection acceptor add_connection (s_bsd.c:1638) and its
file-static clone-rate callee check_clones (s_bsd.c:1572, CLONE_CHECK) to
ircd-common/src/s_bsd.rs via the s_bsd_link.o partial-compile seam
(-DPORT_S_BSD_ADD_CONN_P7bb). check_clones is static (no cref_ oracle) so it
becomes a private Rust twin (the P7p set_sock_opts / P7z check_init precedent),
keeping its persistent backlog list.
Config-resolved: CLONE_CHECK on (CLONE_MAX=10, CLONE_PERIOD=2), DELAY_CLOSE on,
NO_DNS_LOOKUP set (hostp=NULL; USE_IAUTH alias-forwarding block compiled out),
UNIXPORT off, AF_INET6, linux (getpeername-fail guards report_error behind
errno!=ENOTCONN). All callees already Rust (make_client/add_fd/add_client_to_list
P2, inetntop P1, report_error P7q, add_connection_refuse P7k, delay_close P7f,
set_non_blocking P7d, set_sock_opts P7p twin, start_auth P7x); get_sockhost/
get_client_host/sendto_flog stay C externs; sendto_flag is called (variadic, P8).
feat(p7oo): port c_ircd_main boot spine from ircd.c to Rust — P7 COMPLETE
Port `main` (renamed `c_ircd_main` via `-Dmain=c_ircd_main`, ircd.c:837) to
ircd-common/src/ircd.rs as `#[no_mangle] c_ircd_main`, config-resolved to the
locked build (CHROOTDIR/ZIP_LINKS/CMDLINE_CONFIG/USE_SYSLOG/DEBUGMODE/IRC_UID/
IRC_GID OFF; USE_IAUTH/TKLINE ON; IRCDCONF_DELIMITER='|'): sbrk0/uid setup, the
argv flag-parse loop, the setuid-root guard, the vfork/execl iauth-presence
check, the init*/initconf/listener-scan/M-line/SID/split checks + make_isupport,
setup_me/check_class/ircd_writetune, the BOOT_INETD block, the startup banner,
mysrand/daemonize/logfiles_open/write_pidfile/dbuf_init, and `loop { io_loop() }`.
Guarded out of ircd_link.o by -DPORT_IRCD_MAIN_P7oo; ircd-rs/src/main.rs is
unchanged (still calls ircd_sys::c_ircd_main, which now resolves to the Rust def).
nm confirms `T c_ircd_main` from Rust; ircd_link.o now defines no main/c_ircd_main,
only data globals.
This is the LAST C function with logic. P7 is COMPLETE: all C *logic* is now
Rust; only data globals + the ~25 variadic sender trampolines remain (deleted in
P8 when call sites adopt a non-variadic Rust sender API).
Faithfulness: me.serv->sid is [c_char;5] (an array) so C's `!me.serv->sid` is
always-false dead code — reproduced as a never-taken branch. configfile/tunefile
keep their C static-initializers. vfork kept to match C. flags is c_long,
aConfItem::status is u32.
L2-gated, no L1 (the boot spine forks iauth, daemonizes, never returns — the
P7mm/P7jj precedent): the full ircd-golden suite (86 tests) boots through it
byte-identical vs reference-C. The lone failure (golden_s2s_s_serv_stats "sq"
field) is the documented pre-existing p5-s2s-stats-flake (uninitialized-sendq
garbage), proven not a regression by re-running on clean HEAD.
feat(p8b): rip out the sendto_serv_butone + sendto_ops_butone trampolines
The two coupled server-broadcast variadic senders (send.c:517/989). ops_butone
was serv_butone's only in-send.c caller, so porting serv_butone forced porting
ops_butone too to close the C->C edge (no transitional shim).
Non-variadic Rust cores in ircd-common/src/send.rs + a wire!/WirePart raw-byte
line builder (faithful: quit/kill comments pass through verbatim, no UTF-8 lossy
step). 18 call sites (parse/s_misc/s_user/channel/s_serv) build the body with
wire!. Both C bodies #ifdef'd out of send_link.o
(-DPORT_SEND_SERV_BUTONE_P8b/-DPORT_SEND_OPS_BUTONE_P8b); the cref oracle keeps
both for L1.
Gate: golden_s2s_{away,quit,nick,kill,wallops,membership,join,save,
s_serv_connect,s_serv_squit} all byte-identical; cargo build 0 warnings.
feat(leveva): AWAY command (P11 slice 15) — 301/305/306, +a, WHO G/H, WHOIS 301
Add the AWAY command and away-state, the 15th P11 leveva slice. An away message
is stored in the shared registry record (read by other connections), the
server-managed +a (FLAGS_AWAY) bit is toggled in lockstep, and away-ness is wired
into its three observers:
- AWAY :msg -> 306 RPL_NOWAWAY (truncate to TOPICLEN=255, char-boundary safe);
bare/empty AWAY -> 305 RPL_UNAWAY (unconditional, faithful to m_away).
- PRIVMSG (not NOTICE) to an away nick -> 301 RPL_AWAY to the sender.
- WHO shows G (away) / H (here); WHOIS inserts 301 after 312.
+a is server-managed: MODE keeps ignoring 'a', only AWAY toggles it. Grounded in
the original C (b88264f3^: m_away/send_away/who_one/send_whois) + the P5g oracle
port. Unit + boot-golden + proptest (fuzz) coverage; leveva-only, no shared C state.
Gate: cargo test -p leveva (344 lib + golden + proptest) + -p leveva-integration
(10) green; clippy clean; workspace build 0 warnings.
feat(leveva): OPER command (P11 slice 16) — 381/491/464, +o/+O, WHOIS 313
Authenticate `OPER <name> <password>` against the configured operator blocks
(name IRC-fold + user@host mask glob + Password::verify), granting +o (or +O for
a local operator) with a MODE echo + 381 RPL_YOUREOPER, mirrored into the shared
registry so WHOIS reports 313 RPL_WHOISOPERATOR (after the 301 away slot). The
491-vs-464 split distinguishes "no O-line for your host" from a wrong password.
- Password::verify: Plain → byte compare; Hashed → pwhash::unix::verify (new
pure-Rust crypt dependency; libc's crypt isn't available and leveva links no
libcrypt). ServerContext carries the operator blocks from config.
- WHOIS gains 313 in send_whois order (311 [319] 312 [301] [313] 318).
- Grounded in the oracle m_oper/find_Oline/send_whois (git b88264f3^, P5 port).
- Tests: password verify (+2), command/oper.rs (+8 with inverses), whois (+2),
golden_oper boot test, oper_proptest fuzzing (2 properties, 400 cases).
- Documented divergences: no class oper limit, no LUSERS oper count, no S2S
+o propagation; any matching block may authenticate (not strictly the first).
Gate: cargo test -p leveva 417 + leveva-integration 10 green; clippy clean;
cargo build --workspace 0 warnings.
feat(leveva): post-registration command dispatch
Registered connections now route to a real dispatcher instead of the 451
'not registered' stub the registration slice left behind.
- Session gains a Phase::{Registering,Registered} split; on RegStep::Complete
it emits the burst, bumps the counter, and transitions to Registered{nick,
user,host}. A registered connection never re-enters Registration::apply.
- New pure command.rs: PING/PONG, VERSION (351+005), LUSERS, MOTD, USER->462,
and unknown/not-yet-implemented->421 as the WIP fallback. lusers/motd_lines
made pub(crate) for reuse.
- 11 command unit tests + 2 session regression tests (registered PING->PONG not
451; registered unknown->421; pre-reg JOIN still 451).
feat(leveva): user modes (MODE <nick> → +i/+w) + config default +iw
P11 slice 14: the first *user*-mode slice. `MODE <nick>` was a stub
(221 `+` / 502); now a client has real, tracked user modes, a
configurable registration default, and +i (invisible) affects WHO.
- State: a u32 FLAGS_* bitmask (reusing the P10 UserMode enum) on
command::Registered (authoritative) + registry::ClientRecord (the
WHO-visible snapshot), synced by Registry::set_modes.
- Handler (command/mode.rs): faithful to the oracle m_umode resolved
to the client-toggleable set — other nick → 502; own nick query →
221 +<render>; change string applies i/w, silently ignores o/O/r/a
(no 501), 501s any unknown letter; on a net change echoes
:nick MODE nick :<diff> (send_umode sign-grouped diff) + mirrors to
the registry. No change → no echo.
- Config: `default-user-modes` (options block), restricted to i/w,
absent → +iw; Options::Default matches so a config with no options
block is also +iw (the out-of-box default). Seeded silently at
registration; documented in examples/*.kdl.
- WHO: +i users are omitted from the user sweeps (no-arg/0/*, exact,
glob) unless the requester shares a channel or is them; channel WHO
is unaffected. Retires the "no +i filtering" divergence.
Tests: +10 umode unit (with inverses), +4 WHO invisible-filter unit,
+1 config matrix, golden_usermode boot snapshot, usermode_proptest
(3 properties / 400 cases: model lockstep, WHO visibility matrix,
arbitrary-bytes panic-freedom). golden_who regenerated — carol now
hidden from the global sweep (a clean +iw-default demonstration); the
12 direct-ServerContext proptest harnesses get default_user_modes: 0.
cargo test -p leveva (328 lib + golden + proptest) + -p
leveva-integration green; clippy clean; workspace 0 warnings.
feat(leveva): DIE + RESTART + REHASH oper control-plane (P11 slice 25)
The first leveva commands that act on the whole process. Introduces a server
control plane (leveva/src/control.rs: a process-global ServerControl =
Notify + latching AtomicU8) — the process-level analogue of slice 19's
per-connection eject plane.
Faithful to the IRCnet 2.11 m_die/m_restart/m_rehash msgtab gate
({ m_nop, m_nopriv, m_<cmd>, m_nop, m_unreg }): a non-oper -> 481; an oper ->
the handler, which re-gates on is_allowed(ACL_DIE|RESTART|REHASH) -> also 481
without the flag. This wakes the dormant OperPrivilege machinery (parsed since
slice 16): Registered gains a `privileges` set, populated from the matched
block on a successful OPER, and each control command gates on its OperPrivilege
(a non-oper's empty set => 481, reproducing both oracle arms in one check).
- DIE/RESTART (shutdown_all): eject every registered client (issuer included)
with a `Server (Terminating|Restarting). <nick>[<user>@<host>]` NOTICE as the
eject wire, then control::request(Die|Restart). main's select! drains the
ejects over a 200ms grace then exits (Die) or reexec()s the binary (Restart,
CommandExt::exec; CLOEXEC sockets re-bind cleanly).
- REHASH: 382 RPL_REHASHING + re-read/validate the on-disk config.
Documented divergences: killer as <nick>[<user>@<host>]; single-server (no
server-link ERROR branch); RESTART re-exec is review-covered not golden-tested;
REHASH only validates the re-read (applying it to the immutable ServerContext
is deferred); no SCH_NOTICE; KILL/WALLOPS still gate on the oper bit only. No
leveva-integration differential (all three are impure into global buffers).
Verified: unit (control 8; die/restart/rehash 3 each w/ inverses; OPER stores
the set) + control_proptest (gate / eject-conservation / panic-freedom) +
golden_control (mortal 481, oper 382, oper DIE -> NOTICE then exit) + live
boots (DIE->EOF->exit; RESTART->re-exec->port re-bind). cargo test -p leveva
green (lib 468 + all golden/proptest bins), clippy/fmt clean, workspace 0
warnings, leveva-integration unchanged.
feat(leveva): TIME + ADMIN + INFO commands (P11 slice 22)
Server-information query commands, ported as a pure read-only trio (previously
caught by dispatch's 421 fallback). All three reach the CLIENT and OPER msgtab
columns (no oper gate) and ignore the single-server hunt_server target arg.
- TIME -> a single 391; the time renders as unix seconds (leveva's wire-time
convention, like 333/312/314). Pure time_at(now) core + a thin clock-reading
wrapper.
- ADMIN -> the 256/257/258/259 block from a new ServerContext.admin config
field (257=name, 258=location, 259=email); 423 when all fields are empty
(unreachable -- the admin block is mandatory with name+email required).
- INFO -> leveva's own 371* description block (with an On-line since <created>
line) then 374; leveva content, oracle shape (as with VERSION's URL).
The new admin field touched the 19 proptest struct-literal sites. Canonicalizer
gained a 391-seconds + On-line-since mask. Verified by unit tests, a
golden_time_admin_info boot golden, and a 4-property server_query_proptest
(512 cases): TIME shape, ADMIN block-xor-423, INFO 371*-then-374, and
arbitrary-input panic-freedom.
No leveva-integration differential (m_time/m_admin/m_info sendto_one into global
buffers -> no pure oracle entry point).
Gate: cargo test -p leveva green (lib 429 + all golden/proptest binaries);
clippy --all-targets clean; fmt clean; build --workspace 0 warnings.
feat(p8q): drop ircd.o outright — ircd.c data globals → Rust statics
Over P7c..P7oo every ircd.c *function* went Rust, leaving ircd_link.o
defining only data globals (nm shows no T symbols). The fourth P8
data-global drop (after s_auth.o/send.o/s_bsd.o) moves them to Rust-owned
#[no_mangle] statics in ircd-common/src/ircd.rs with byte-exact C inits:
me/client/istat/iconf/myargv/rehashed/portnum/configfile/debuglevel/bootopt/
serverbooting/firstrejoindone/sbrk0/tunefile, the dorehash/dorestart/
restart_iauth signal flags, the next* event timers, and ListenerLL.
- me self-pointer hazard is a non-issue at definition: me.name->serv->namebuf
is set at runtime by make_server/setup_me, so the static is a zeroed
aClient (MaybeUninit::zeroed) and client = addr_of_mut!(me) in const.
- data-symbol seam carries the bindgen-surfaced globals (other modules'
ircd_sys::bindings::* extern decls resolve to these defs at link); the 5
non-bindgen flags/timers, formerly local extern "C" blocks in ircd.rs, are
now module statics (bare refs resolve directly); the three import lists
trimmed of the now-defined names.
- configfile/tunefile default to IRCDCONF_PATH/IRCDTUNE_PATH via new
IRCD_CONF_PATH/IRCD_TUNE_PATH rustc-envs (ircd_sys::CONF_PATH_Z/TUNE_PATH_Z,
NUL-terminated).
- build.rs: ircd.o added to PORTED; the ircd_link.o second-compile (the long
-DPORT_IRCD_* chain) + the ircd.o->ircd_link.o link-map arm deleted. cref
keeps the unguarded ircd.o for the L1 differentials.
Gate (pure-data drop, per P8n/o/p): the ircd L1 differentials
(try_connections/check_pings/calculate_preference/delayed_kills/ircd_signals/
ircd_tune/ircd_cli_helpers/setup_signals/activate_delayed_listeners) all
zero-diff + golden_registration byte-identical; nm shows all ~26 globals as
Rust defs (B/D, none U) with the correct .data/.bss split; workspace builds
0 warnings. Only support_link.o's format remnants remain C.
feat(leveva): WHOX + remote-user WHO/WHOIS fields (P11 slice 47)
Implement WHOX in WHO and fix the coupled classic-WHO/WHOIS parity gaps
that surfaced once S2S landed after these handlers were written:
- parse the WHO second arg (oracle parse_who_arg): the legacy `o`
opers-only filter + the `%fields[,token]` WHOX selector (≤3-char token).
- emit `354 RPL_WHOSPCRPL` field-by-field in the oracle's fixed who_one
order, only the requested fields. Single-server divergences match the
oracle constants: %i (IP) re-uses host (no DNS-resolved host), %l idle=0,
%a account=0, %o op-level=n/a.
- classic `352`: add the `*` oper flag to the status, and resolve a remote
user's own server-name / hopcount / SID from the Network mirror instead of
hardcoding us / 0 (the load-bearing remote-field fix). Same for WHOIS 312
(real remote server name + burst description).
- advertise WHOX in ISUPPORT (005).
Pinned by 19 new who.rs unit tests (parse, 354 rendering, oper `*`, the
`o` filter, remote fields — each with its inverse) + the whois remote-312
test + the isupport WHOX test. No leveva-integration differential: the
oracle entry points are unsafe/global-buffer (as with every leveva-native
slice). Plan: docs/superpowers/plans/2026-06-09-p11-slice47-*.md.
feat(leveva): PASS command + connection-password gate (P11 slice 49)
Wire the connection-block password (the C I-line password) into registration.
leveva already parsed allow{ password … } blocks but never consulted them and
had no PASS command, so a passworded allow was silently unenforced.
- registration.rs: Registration stores PASS (last-wins, like the oracle's
strncpyzt overwrite); bare PASS -> 461; the password is carried in
RegStep::Complete. New pure connection_password_ok() matcher: first matching
allow decides, a wrong+fallthrough passworded rule defers to the next, no
matching passworded rule -> default-allow (purely additive, no I-line
enforcement).
- session.rs: finalize_registration runs the gate after username resolution and
before the K-line gate (the oracle's check_client order); a mismatch emits
464 ERR_PASSWDMISMATCH + ERROR close, claims no nick, counts nobody. The
carried PASS survives the CAP/auth deferred-finalize. Identity bundled into
PendingComplete to keep the arg count in bounds.
- command/mod.rs: a post-registration PASS is refused with 462, like re-USER.
Unit tests cover the matcher truth table (default-allow, no-pw match, correct/
wrong/missing password, fallthrough defer + exhaustion) and the session gate
(welcome / 464 / regression / 462 / CAP-suspension composition).
feat(leveva): IRCv3 message-tags foundation (P11 slice 56)
Add tag support to the core Message type and advertise the `message-tags`
capability — the prerequisite that unblocks the deferred slice-55 bot tag plus
server-time/message-ids/message-redaction/oper-tag.
message.rs:
- Message/MessageBuilder carry ordered `tags: Vec<(String,String)>`; `parse`
peels a leading `@key=value;...` segment (length-checked against
MAX_TAG_DATA_LEN=8191 → ParseError::TagsTooLong, unescaped per the spec table),
`to_line`/`to_wire` render it (server tags before client `+` tags, by insertion
order); `tags()`/`tag(key)` accessors; builder `.tag(k,v)`.
- to_wire caps only the body at 512; the tag prefix is separate budget per spec.
- Escaping table both ways: `;`<->`\:`, ` `<->`\s`, `\`<->`\\`, CR<->`\r`,
LF<->`\n`; on unescape `\<other>` drops the backslash, lone trailing `\` drops.
cap.rs: advertise `message-tags` in SUPPORTED; `ClientCaps.message_tags`.
This is behavior-bearing on its own: the moment the cap is advertised, tag-capable
clients send `@tag PRIVMSG ...`, and the server now parses the `@tags` segment
correctly (it would otherwise be mistaken for the command). A client-only tag is
parsed and currently dropped; TAGMSG + client-only tag relay (per-recipient cap
gating + S2S) and the 417 reply are slice 57.
Tests (TDD, inverse invariants): message.rs units (tag/body split; untagged line
byte-identical; escaping round-trip; valueless/empty-value normalization;
render->parse->render stable; TagsTooLong; body-cap-with-tag-prefix; lone/unknown
escapes). cap.rs units updated for the new LS token. Fuzzing:
message_tags_proptest.rs (render/parse round-trip, escaping round-trip,
never-panic on arbitrary @input, untagged-line no-regression). Regenerated the 2
golden_cap snapshots.
cargo test -p leveva 814 lib + proptests green; clippy clean; build warning-free.
feat(leveva): IRCv3 MONITOR / the online-offline watch list (P11 slice 70)
Adds the MONITOR extension: a per-connection nick watch list with server-pushed
730/731 online/offline notifications, replacing ISON polling. Advertised via the
MONITOR=100 ISUPPORT token (not a CAP). New leveva::monitor shared index (keyed by
watcher UID, off the Registered struct), command/monitor.rs handler (+/-/C/L/S,
734 overflow, comma-chunked replies), live-notify hooks at the registration /
disconnect / nick-change seams, numerics 730-734, and help/MONITOR.md.
Single-server-local: live pushes fire on local lifecycle events only (remote S2S
transitions deferred). Gate: monitor module + command + seam units, golden_monitor
(two clients), monitor_proptest; full leveva suite 1222 pass, clippy clean, 0 warnings.
feat(leveva): IP cloaking / the +x hidden-host usermode + user modes on connect (P11 slice 72)
Ports Elemental-IRCd's extensions/ip_cloaking.c to leveva as a client-settable
`+x` usermode that hides a client's host behind a deterministic FNV-keyed
scramble of its original host. The host change is distributed by reusing the
slice-71 chghost plane, so a cap co-member sees an in-place CHGHOST and the
network an ENCAP CHGHOST.
- cloak.rs: faithful port of the C — fnv_hash(s,32), cloak_ip (keep IPv4 first
two octets / IPv6 first half, substitute the rest from the g–z alphabet),
cloak_host (scramble first-label letters then digits), dispatch on IpAddr.
Wire-safety: never introduces a leading ':'.
- mode.rs/numeric.rs: UserMode::Cloak (+x, 0x0100, appended last); 396
RPL_HOSTHIDDEN. +x is NOT in SEND_UMODES — it propagates as a host change,
not a umode diff (no double-propagation).
- registry.rs: immutable ClientRecord.orighost (set at try_claim, never by
set_host) so the cloak is computed from / reverts to the real connect host.
- command/mode.rs: 'x' joins the i|w|B settable arm; apply_cloak_change cloaks/
restores the host, mirrors set_host, distributes over the chghost plane, and
emits 396 (is now your hidden host / hostname reset).
- config/parse.rs: default-user-modes now accepts 'x' so a network can
auto-cloak everyone; a default-+x client is cloaked at registration (the C
check_new_user path).
- session.rs: clients are now told their user modes on connect (:nick MODE nick
:+<modes> after the welcome burst).
leveva-native (no C oracle) → unit + golden + proptest, no differential. Tests:
cloak/mode/numeric/registry/command/session/config units + golden_ip_cloaking
(127.0.0.1 -> 127.0.y.p) + cloak_proptest (the fuzz target). 4 registration
snapshots regenerated (004 -> oOiwraWBx + the trailing MODE notice). clippy
clean, 0 warnings.
feat(channel)!: drop the IRCnet channel types (&, !, +); # only (P11 slice 144)
Leveva inherited four channel prefixes from IRCnet 2.11 — # (global), &
(server-local), + (modeless), and ! (timestamped "safe"). The latter three
are IRCnet-specific and unwanted; this removes them so only # is a valid
channel prefix. CHANTYPES is now "#".
With +/! gone, the *modeless* machinery they were the only users of is dead
and is deleted: use_modes() removed, a channel creator is always a chanop,
and the unreachable KickGate::NoChanModes / ApplyOutcome::NoChanModes variants
(and their 477 handler arms) are gone. ERR_NOCHANMODES stays in the numeric
table (a wire-code registry, not a feature list).
The three S2S channel-target sigil helpers narrow from #&+! to #, and
relay::local_join drops its dead &-channel non-propagation skip.
Fuzzing: new tests/chantypes_proptest.rs (512 cases) pins that any legal
#-body parses and round-trips, and — the inverse — any &/!/+ -prefixed name
is BadPrefix regardless of body, no name resurrects a dropped type, and
arbitrary bytes never panic the validator.
Docs: CHANNEL_TYPES/KICK/REOP_LIST help pages + 8 ISUPPORT golden snapshots.
Gate: cargo test -p leveva green; clippy -p leveva --tests clean;
cargo build --workspace 0 warnings.
Plan: docs/superpowers/plans/2026-06-13-p11-slice144-drop-irc-net-channel-types.md
feat(leveva): IRCv3 labeled-response (+ batch dependency) (P11 slice 155)
Echo a command's inbound `@label` tag back on its direct reply so a client
can correlate request->reply across interleaved output. The pure
`leveva::labeled::wrap` runs over the `command::dispatch` reply vector at the
session seam and renders the spec's three shapes: 0 replies -> `@label ACK`,
1 -> the lone reply tagged in place, >=2 -> a `labeled-response` BATCH
(`+<ref>` open carrying the label, `@batch=<ref>` inners, `-<ref>` close).
The batch ref is a process-global hex counter (the msgid precedent),
generated by the caller so `wrap` stays pure (the fuzz target).
Advertise both `labeled-response` and `batch` (the wire dependency); the
wrap gate is `labeled_response` only, `batch` is wired-but-not-read (a future
dedicated batch slice adds other batch types). New Message::with_label /
with_batch front-prepend helpers.
Scope: the synchronous direct-reply path only. Async mailbox lines
(echo-message, channel fan-out) travel a different path and are not in a
labeled batch -- a labeled PRIVMSG yields ACK (its echo, if negotiated,
arrives separately untagged), which satisfies the spec's self-message
MUST-NOT-label rule conservatively. leveva-native (the oracle predates CAP).
Tests: labeled/message/cap units + labeled_response_proptest (600 cases over
pure wrap) + golden_labeled_response (real binary: WHOIS-batch / TIME-single /
PRIVMSG-ACK / non-negotiating inverse) + a batch=<REF> canonicalizer mask.
The real ircv3.net spec lives at docs/rfcs/ircv3/labeled-response.md.
Gate: cargo test -p leveva green (229 binaries, 0 fail), clippy clean,
cargo build --workspace 0 warnings.
feat(leveva): server-notice user mode +s (P11 slice 184)
Closes the recurring "leveva has no server-notice masks yet" deferral named in
command/kill.rs, command/rehash.rs, and the SAMODE/SAPART slice plans.
Re-introduces the oracle's +s (FLAGS_SERVNOTICE) user mode — dropped while server
notices were unimplemented — and a sendto_flag-equivalent fan,
snotice::server_notice, that delivers `:<server> NOTICE <nick> :*** Notice -- <text>`
to every local +s user. +s is operator-gated to set and purely local: it is excluded
from SEND_UMODES so it never crosses a link, which makes the registry fan naturally
select only local users. It now surfaces in RPL_MYINFO (oOiwraWBx -> oOiwraWBxs).
Wired the two clearest deferred call sites: a local oper KILL posts "Received KILL
message for <victim> from <killer>: <comment>"; REHASH posts "<nick> is rehashing
server config file". The inbound-S2S-KILL notice and the SA* family notices stay
documented follow-ons.
Tests: snotice units (+ inverse -s), umode oper-gate units (+ non-oper-ignored
inverse), kill/rehash notice units (+ failed-command-no-notice inverse), the
SEND_UMODES-excludes-+s assertion, snotice_proptest (conservation/fidelity/-s
inverse), and golden_snotice (boot-level). Updated five welcome-burst snapshots +
the myinfo_004/isupport units for the new umode string.
feat(leveva): KNOCK per-channel flood throttle (712 ERR_TOOMANYKNOCK) (P11 slice 200)
Close the last KNOCK follow-on (the slice-198/199 deferral). A KNOCK to a
restricted channel delivered unconditionally, letting a user spam a channel's
operators by re-knocking in a loop. A channel now has a per-channel cooldown
(knock_throttle::KnockThrottle, charybdis knock_delay_channel, 60s): it admits
at most one KNOCK per window; a knock inside the window is refused with 712
ERR_TOOMANYKNOCK and notifies nobody (local ops and, via S2S, remote ops).
The store is clock-injected (caller passes unix-second now, like kline/
autoconnect) so it is deterministic under test, case-folds the channel key, and
self-prunes expired holds so it never leaks. Throttle is local-path only by
design: an inbound relayed ENCAP * KNOCK is unthrottled (the origin server
already throttled its own user; charybdis throttles per-server). The per-user
knock_delay stays out of scope. The KNOCK family (198/199/200) is now complete.
Adding the shared ServerContext.knock_throttle field touched the two
command::testutil literals and every integration-test ctx() literal (they build
ServerContext by hand, not via from_config).
Tests (RED first, inverse invariants, fuzzing): 7 knock_throttle units +
2 command::knock units + tests/knock_throttle_proptest.rs (PROPTEST_CASES=2000).
Gate: cargo test -p leveva --lib 1915 pass/0 fail; clippy --tests clean;
build --workspace 0 warnings.
feat(leveva): command-rate flood protection — the IRCnet "fakelag" penalty clock (P11 slice 201)
Ports the IRCnet 2.11 command-rate flood protection, deferred since the start of
the migration. The smoking gun: the oper privileges `CanFlood`/`NoPenalty` were
parsed from `operator {}` blocks but read nowhere — dead, awaiting this clock.
Faithful to the oracle (`common/parse.c` + `s_bsd.c`, verified in git history):
each command charges `1 + len/100` onto a per-connection penalty clock that
idle-decays to real time, so only a faster-than-1-cmd/sec client accumulates; a
sustained flood is closed with the oracle's exact
`ERROR :Closing Link: <host> (Excess Flood)`. Opers holding `can-flood` /
`no-penalty` (the now-live dead privileges) are exempt.
- new `flood::FloodClock` — pure, now-injected, deterministic/fuzzable
- `Session` charges in the registered-dispatch branch; armed by the serve loop
(`main.rs`/`websocket`), NOT `Session::new` — the `heartbeat` opt-in precedent
and the leveva analogue of the C `ircd_no_fakelag` test flag (in-process
proptests fire synthetic instant bursts that would always trip the guard)
Kill-only: the fakelag *delay* half, a per-command cost table, and a pre-reg
count guard are documented follow-ons.
Tests: 6 flood units + 3 session units + a 3-property proptest (model-lockstep
vs an i64 reference clock, PROPTEST_CASES=4000) + a golden boot test (real binary
floods → Excess Flood). cargo test -p leveva green; clippy clean; 0 warnings.
fix(leveva): burst NJOIN carries the channel TS — P11 slice 215
A channel a server learned **for the first time in a burst** was re-clocked to
the receiver's local nanosecond clock instead of adopting the origin's
`created_at`: the leading `CHANTS` is dropped on an unknown channel
(`apply_encap_chants` → `ChanTsOutcome::Unknown`), and the burst `NJOIN` was
TS-less, so `handle_njoin` stamped `clock::unixnano()`. Two servers then
disagreed on a channel's TS by *when each learned it*, and the next merge
deopped the later learner — the divergence slice 167 fixed for the live NJOIN,
left unfixed on the burst path. Observed in prod: updating a server to v0.1.0
and relinking stripped `#xe`'s ops + `+R` that should have survived the merge.
Fix: the burst `NJOIN` now carries the channel TS as a middle param
(`:<sid> NJOIN <chan> <ts> :<members>`, the live form since slice 167) so a
peer adopts the origin's TS. Safe w.r.t. the merge case despite the slice-167
caveat: the burst is CHANTS-first (slice 171), so `we_lost` reads the
pre-merge `created_at` before NJOIN runs, and `njoin_member` only stamps on
creation — so for an existing channel the NJOIN-TS is a no-op (CHANTS owns
arbitration), and it only supplies the TS for a newly-learned channel.
`njoin_member_chunks` subtracts `1 + ts_digits` from the line budget; the boot
canonicalizer masks the new `NJOIN <chan> <ts> :` TS like CHANTS/USERTS.
Tests: burst_channel_ts_is_adopted_not_reclocked (the regression),
burst_njoin_line_carries_the_ts_middle_param, …_does_not_clobber_an_existing
_channels_arbitrated_ts (inverse merge), and a fuzz round-trip over arbitrary
origin TS. Updated the two burst snapshots + golden_s2s_njoin_live.
Gate: cargo test -p leveva green (pre-existing golden_s2s_keepalive BrokenPipe
flake aside); clippy clean; workspace build 0 warnings.
feat(leveva): caller-id (+g user mode + ACCEPT) — P11 slice 216
Add charybdis-style caller-id ("server-side ignore"): a user sets +g to
receive private PRIVMSG/NOTICE only from clients on their ACCEPT list. A
blocked PRIVMSG bounces 716 to the sender and notifies the target once
(717 to sender + 718 to target); a NOTICE is silently dropped.
The pure CallerId store (accept set + once-per-pair notify gate) lives
inside Registry — the metadata precedent — so it costs no ServerContext
churn; accept entries are stable UIDs (survive nick changes, quit-filtered
on read), capped at 30. +g is purely local (not in SEND_UMODES): the gate
runs on the target's home server, so a transit/origin server needs no copy.
ACCEPT supports add / -remove (mixed) / list (281+282), with 401/456/457/458;
self-accept is a no-op. CALLERID=g advertised in 005; 8 new numerics.
Picked after confirming no clean documented deferral remained open — the
STS-REHASH / S2S-KILL-notice / throttle-knob / MODE+VERSION-proptest /
UTF8ONLY "deferred" doc comments were all stale, closed by later slices.
Remote-sender gating (S2S) and +G soft caller-id are documented follow-ons.
Tests: callerid unit (6), accept (8), message gate (5), numeric round-trip,
isupport token, s2s/umode excludes +g; golden_callerid (2); callerid_proptest
(2, accept-list + notify model lockstep). Boot snapshots regenerated for the
new 004 'g' umode letter and the 005 CALLERID=g token.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
feat(leveva): METADATA SET/CLEAR rate limiting (FAIL RATE_LIMITED) — P11 slice 228
Close the draft/metadata-2 follow-on named verbatim in command::metadata's
divergence list: a client that floods METADATA SET/CLEAR is now refused the
excess with `FAIL METADATA RATE_LIMITED <target> <key> <retry-after>` and the
mutation is not performed.
Pure core metadata::rate_charge is a flood::FloodClock-style per-user penalty
clock (cost 1s, burst 60s -> 61 mutations, comfortably above MAX_KEYS=30 so a
full profile setup is never throttled, then ~1/s sustained). State lives in
MetadataStore (rate map + charge_rate/forget_rate), cleaned on quit via
Registry::release. Gate wired into set_cmd (before reading the value, covers
set + removal) and clear_cmd (key field `*`); opers exempt, inbound-S2S
naturally exempt.
Tests: metadata.rs units (burst/refusal/decay/retry-after rounding, store
round-trip + forget reset), command units (SET flood limited + value not
stored, oper exempt, CLEAR gated, single SET no false positive), and
metadata_rate_proptest fuzzing the pure decision (burst rule, no-budget-on-
refusal, exact advance, monotone-in-time).
feat(leveva): +S TLS-only channel mode — P11 slice 233
A leveva-native channel flag porting charybdis chm_sslonly (MODE_SSLONLY): a
JOIN of a +S channel by a client NOT on a secure (TLS) transport is refused
519 ERR_SSLONLYCHAN :Cannot join channel (+S) - SSL/TLS required and the client
is not added. Builds on slice 227's +Z secure user mode and the +C/+c flag-mode
wiring (220/232).
A hard JOIN gate like +O: checked in Channels::check_join alongside +O, before
the invite override, so INVITE/+I do not bypass it (a transport requirement,
not an access list); ops not exempt; existing members keep their seat (only new
joins gated, matching charybdis's can_join hook). KNOCK to a +S channel is
refused like +O.
The secure signal is the joiner's +Z (UserMode::Secure) bit via
mode::is_secure(attrs.umodes), NOT attrs.is_ssl — the latter is certfp-based
and undercounts TLS users with no client cert (the gap slice 227 closed). $z
extban semantics are left untouched. check_join runs for local joins only;
remote joins arrive via NJOIN and bypass it.
ChanMode::SslOnly ('S', bit 0x4000000, MODE_SSLONLY, Flag) wired through both
flag-mode allowlists (mode.rs ALL/as_char/bit/mode_name/from_char +
command::mode::supported_flag). JoinReject::SecureOnly → join_reject maps to the
new ErrSslonlychan = 519. CHANMODES/004/MYINFO auto-derive S.
Pure seam channel::sslonly_blocks(set, secure). Tests: 2 unit
(check_join_enforces_ssl_only incl. INVITE-no-bypass + -S round-trip;
sslonly_blocks_truth_table), sslonly_proptest (gate keys only off +Z over
arbitrary u32 umode masks; plain channel never gates; toggling +Z flips),
golden_sslonly end-to-end (plaintext JOIN → 519, not added, NAMES confirms; -S
re-opens), 9 regenerated 004/005 snapshots (chanmode string only).
cargo test -p leveva 2298 pass; clippy clean; workspace builds 0 warnings.
feat(leveva): +j join throttle channel mode — P11 slice 234
Charybdis-style `+j n:t` (MODE_JOINTHROTTLE): a channel admits at most n
joins per t-second window; the next join bounces 480 ERR_THROTTLE and is
not added. The first parameter-bearing flag mode since +l/+k and the first
JOIN-rate gate; continues the channel-mode parity track (220 +C, 232 +c,
233 +S). leveva-native — gate is unit + golden + proptest.
- Pure core join_throttle.rs: parse_param(n:t) + format_param + the charybdis
fixed-window decide() (both fuzz seams); JoinThrottle counter store (reads no
clock, self-draining) housed inside Channels (zero ServerContext-literal churn).
- ChanMode::JoinThrottle ('j', bit 0x8000000, Param kind) threaded through both
mode parsers + the three ModeChange matches + 324/CHANMODES-type-C/004 render.
- JOIN gate in command::join::join_one: non-members, local path only (remote
NJOIN gated at its home server); SAJOIN bypasses; refusal -> 480 ErrThrottle.
- Tests: 12 core units + 2 channel + 1 mode units; jointhrottle_proptest (5,
incl. no-window-over-admits) + golden_jointhrottle; 8 005/004 snapshots
regenerated (CHANMODES ...,k,lj,... / 004 ...IRklj, token-only).
feat(leveva): +z op-moderation channel mode — P11 slice 235
A port of charybdis chm_simple MODE_OPMODERATE. On a +z channel a message
that would be blocked from reaching the channel — by +n (external), +m
(moderated), or a +b ban/quiet (CanSend::External/Moderated/Banned) — is not
refused 404; it is delivered to the channel's operators (rank >= +o) only, so
ops can see what muted, banned, or external users are saying. Ok/NoSuchChannel
verdicts are never redirected. Extends the speech-plane can_send work of slices
214/220/232.
- mode.rs: ChanMode::OpModerate ('z', bit 0x10000000, Flag kind, MODE_OPMODERATE);
CHANMODES + 004 chan-mode string auto-derive from ALL/kind().
- command/mode.rs: 'z' in the supported_flag allowlist (the recurring flag-mode
gotcha — needs both the ChanMode and this second allowlist or MODE +z 472s).
- channel.rs: pure opmod_redirects(set, verdict) + Channels::op_moderated /
op_members (rank >= op).
- command/message.rs: the block arm redirects to local ops when op_moderated —
no 404, no echo, no relay (TAGMSG/REDACT + S2S op-redirect are documented
follow-ons), else the existing 404/silent.
Tests: mode/channel/command-mode units (5) incl. inverses; fuzz opmod_proptest
(opmod_redirects vs an independent model over every (bool, CanSend); op_members
exactly the rank>=op set over arbitrary membership); golden_opmod (real binary:
+mz redirects bob's muted line to the op, not to carol/bob, no 404; -z restores
the 404). Snapshot/assertion churn: isupport/registration/config literals gain z
(+z is now an accepted default flag; unknown-letter test uses '>'); 6 golden
004/005 snapshots regenerated (token-only z).
Gate: cargo test -p leveva green (pre-existing golden_restricted_op read_until
flake aside); cargo clippy -p leveva --tests clean; cargo build --workspace 0
warnings.
feat(leveva): +r registered-only-join channel mode (charybdis MODE_REGONLY) — P11 slice 241
Adds the leveva-native channel flag mode +r: on a +r channel only a client
signed in to a services account may JOIN; a signed-out client is refused 477
(charybdis ERR_NEEDREGGEDNICK, which overloads numeric 477 — reuses
Numeric::ErrNochanmodes since a duplicate-discriminant variant is impossible).
The join-plane sibling of +g; a hard JOIN gate like +O/+S (an INVITE/key/
exception does NOT bypass it — account identity is a user property), so KNOCK to
a +r channel is refused too.
- mode.rs: ChanMode::RegisteredOnly, letter r, bit 0x80000000 (last free u32
bit), Flag kind; added to ALL/as_char/bit/mode_name/from_char.
- command/mode.rs: supported_flag 'r' (the second allowlist).
- channel.rs: pure regonly_blocks(set, has_account) seam + the hard-gate clause
in check_join (reads attrs.account, same source as $a extban); new
JoinReject::RegisteredOnly.
- command/join.rs + command/knock.rs: reject mapping + KNOCK refusal.
- ISUPPORT/MYINFO auto-derive (CHANMODES …SzTgr, MYINFO …SzTgr…); 10 snapshots
+ isupport/registration assertions updated (token-only).
Reclaiming the slice-77 ripped-out reop letter r triggered the
unknown-chanmode-letter hazard: removed the obsolete ripped_out_reop_is_unknown
test, updated chan_mode_bits_match_oracle, switched golden_modes's unknown-mode
probe to +>, and repurposed the modes_proptest AnonReop op to BareAdmin.
Tests: golden_regonly.rs (RED-first), channel/mode/command::mode units (gate +
inverse + hard-gate proof + truth table), regonly_proptest.rs, modes_proptest
FLAGS += RegisteredOnly.
cargo test -p leveva green; cargo clippy -p leveva --tests clean; cargo build
--workspace 0 warnings.
feat(leveva): +f channel forwarding (charybdis MODE_FORWARD) — P11 slice 242
A JOIN refused by a forwardable reject (+i/+l/+b/+r) on a +f channel is
redirected to the forward target instead of erroring: the joiner gets
470 RPL_LINKCHANNEL <from> <to> and a clean single-hop JOIN of the target.
- mode.rs: ChanMode::Forward (letter f, bit 0x10000, Param kind), ALL→28.
- channel.rs: ChannelModes.forward; pure seams forwardable() + valid_forward_target()
(valid ChanName + not-self, fold-aware); ModeChange::Forward applied (set re-validates,
invalid/self silently dropped; trusted-peer verbatim); 324 render + forward_target().
- command/{mode,join}.rs: +f param parse (461 on bare +f, -f clears); join_one gains
allow_forward — single hop, no chaining (SAJOIN/OJOIN pass false).
- numeric.rs: RplLinkchannel = 470.
- S2S: rides the live channel-MODE plane (relay render + s2s/mode parse → apply_modes_as_server);
not in the netburst CHANTS, matching the +j precedent (documented divergence).
- ISUPPORT/004: +f auto-classifies into CHANMODES group C (beIR,k,ljf,…); MYINFO gains f.
Tests: channel/mode/command::mode/s2s::mode units; golden_forward (470 redirect +
inverse); forward_proptest (forwardable + valid_forward_target totality/model);
ISUPPORT/004 snapshots + characterization literals updated for the trailing f.
Divergences: single hop (charybdis chains); invalid/self target silently dropped;
+k bad-key and the hard +O/+S gates never forward; +Q/+F are follow-on slices.
feat(leveva): +Q/+F forward-target control (charybdis MODE_DISFORWARD/MODE_FREETARGET) — P11 slice 243
The two recorded follow-ons to slice 242's +f channel forwarding. Both are
plain channel flag modes that live on the forward *target* and gate forwarding
at two different stages:
- +F (free target, MODE_FREETARGET): charybdis chm_forward requires a local
non-oper setting `+f #target` to be a channel operator (+o) of #target; +F on
the target lifts that requirement. This forces the op-on-target permission
gate that slice 242 deferred — leveva previously let any manager point +f at
any valid channel; now a non-op of the target gets 482 (or 403 if the target
doesn't exist), bypassed by +F / oper-override.
- +Q (disforward, MODE_DISFORWARD): a +Q target refuses incoming forwards — a
would-be-forwarded joiner falls through to the source channel's original
reject (no 470, no forward).
Mechanism:
- mode.rs: ChanMode::DisableForward ('Q', 0x1000) + FreeTarget ('F', 0x2000),
both Flag kind; ALL -> [ChanMode; 30]; bit/as_char/mode_name/from_char.
- channel.rs: pure forward_set_verdict(exists, chanop, free) -> ForwardSetVerdict
(fuzzed); forward_set_verdict_for(target, setter) store accessor (chanop = +o
rank, reads the +F bit); forward_disabled(name) (+Q flag, modelled on
blocks_notice).
- command/mode.rs: supported_flag gains 'Q'/'F'; the ModeReq::Forward set arm
runs the +F-gated check (gated on can_manage && !oper_override, ops-checked-
first like the extban-validity gate) -> 403/482 for the target, change skipped.
- command/join.rs: the forward redirect skips a +Q target (forward_disabled).
- ISUPPORT/004: both auto-classify into CHANMODES group D ->
CHANMODES=beIR,k,ljf,psmntiOCcSzTgrQF; MYINFO chan-mode string gains QF.
Divergences: target-op rank is strictly chanop (+o), so a leveva halfop on the
target does not satisfy the +F gate (faithful to charybdis is_chanop); a
nonexistent/invalid +f target is now 403 for a manager-setter (charybdis
find_channel) where slice 242 silently dropped it, but a self-forward stays
silently dropped at the apply layer. No S2S follow-on — plain flag bits ride the
standard MODE/CHANTS machinery; both gates run at the home server (distributed),
same as +f/+g/+r.
Tests: mode/channel/command::mode unit tests (truth tables, store accessors,
482/+F set gate, +Q set/clear inverse); golden_forward_control.rs (both +F and
+Q scenarios with inverses); forward_proptest.rs gains an exhaustive +
proptest model of forward_set_verdict. 9 CHANMODES snapshots regenerated.
cargo test (edited + snapshot binaries) green; clippy -p leveva --tests clean.
feat(leveva): +P permanent channel (charybdis MODE_PERMANENT/chm_staff) — P11 slice 244
A channel carrying +P is not destroyed when its last member leaves: its
topic, modes, and list masks persist with zero members, so the next joiner
enters an existing channel and is NOT made creator-op. +P is oper-only —
only a server operator (or a SAMODE override) may set or clear it (a
non-oper gets 481, checked before the chanop gate, matching chm_staff).
- mode.rs: ChanMode::Permanent (letter P, bit 0x80000, MODE_PERMANENT, Flag).
- channel.rs: pure destroy_when_empty(flags) + permanent_set_allowed(...)
decision seams (fuzzed); permanent(name) accessor; part/kick/remove_everywhere
skip the empty-channel deletion for +P; join() decides creator-op from
`created`, not members.is_empty() (the permanent-empty-channel fix).
- command/mode.rs: supported_flag gains P; the Flag arm refuses a non-oper
+P/-P with 481.
- isupport/004: CHANMODES group D + MYINFO gain a trailing P.
Divergences: a bare (non-chanop) oper still hits the 482 op-gate — use
SAMODE (the 481-vs-482 precedence is honoured); bursting empty permanent
channels on link is a documented follow-on.
Tests: unit (mode letter/bit/name/kind; channel persistence across
PART/KICK/QUIT + no-op-on-rejoin + flag tracking; command 481 oper-gate),
golden_permanent.rs (persistence + topic preserved + creator-op inverse +
non-oper 481), permanent_proptest.rs (destroy_when_empty + permanent_set_allowed
totality/model), regenerated CHANMODES snapshots.
feat(leveva): +D deaf user mode (charybdis UMODE_DEAF) — P11 slice 247
A +D ("deaf") user receives no channel-addressed messages — PRIVMSG,
NOTICE, TAGMSG, and the +z op-moderation redirect — while private
(nick-target) messages still reach them. Client-settable by any user
(MODE <nick> +D / -D), like +i/+w/+B; no oper gate.
The channel fan-out delivers to local members only on each server, so
the deaf filter runs on the deaf user's home server local fan-out: a
remote server never delivers to a deaf user directly and so never needs
to know. +D is therefore purely local and NOT in SEND_UMODES — the same
precedent as +g/+G/+s.
- mode.rs: UserMode::Deaf (letter D, bit 0x2000, FLAGS_DEAF), threaded
through ALL/as_char/bit/flag_name/from_char/from_bit/Display; new pure
predicate mode::is_deaf (the fuzz seam).
- command/mode.rs: D added to the self-settable umode group + umode_diff.
- command/message.rs + command/tagmsg.rs: skip deaf local members at the
channel fan-out and the +z op-redirect loops.
- isupport: user_modes_string() -> oOiwraWBxsgGZD (004 RPL_MYINFO).
Tests: unit (mode predicate/round-trip, self-settable + clear inverse,
channel-blocked-but-private-delivered + un-deaf round-trip for
PRIVMSG/NOTICE/TAGMSG), boot-golden golden_deaf.rs (sentinel-ordering
proves the channel line is dropped while a direct marker arrives), fuzz
deaf_proptest.rs (is_deaf vs independent model + unique letter/bit).
Regenerated 5 welcome-burst snapshots (004 usermode +D) + 2 hardcoded
assertions; s2s::umode propagation test excludes Deaf.
cargo test -p leveva green; clippy clean; cargo build --workspace 0 warnings.
feat(leveva): +z operwall user mode + OPERWALL command (charybdis UMODE_OPERWALL) — P11 slice 252
The oper-only sibling of +w/WALLOPS: an operator broadcasts OPERWALL to every
+z operator network-wide. Closes one of the named remaining charybdis umodes.
A near-exact mirror of the slice-178 WALLOPS machinery (command/operwall.rs +
s2s/operwall.rs).
- +z (letter "z", bit 0x10000, FLAGS_OPERWALL): oper-gated to set like +s
(non-oper +z silently ignored, -z always allowed); purely local, NOT in
SEND_UMODES (the OPERWALL command propagates and each server fans to its own
local +z — diverges from +w, which is in SEND_UMODES only for oracle
faithfulness).
- OPERWALL <text>: oper-only (481), 461 on empty, fans :mask OPERWALL :text to
local +z, propagates :<uid> OPERWALL :text; inbound fans is_local_uid-gated +
split-horizon relay.
- mode.rs UserMode::Operwall (+ is_operwall predicate); umode_diff/RENDERED;
004 umode literal +z; help/OPERWALL.md + USER_MODES.md row.
- The mode.rs round-trip test also moved off "z" as its unknown example to ">".
Tests: command/operwall.rs + s2s/operwall.rs units (mirror wallops), +z oper-gate
units, golden_operwall.rs (+z oper receives, -z oper and -z sender do not),
operwall_proptest.rs (delivery decided by +z alone). 5 welcome-burst snapshots
gain the z letter.
cargo test -p leveva green; clippy clean; build --workspace 0 warnings.
feat(leveva): +l locops + LOCOPS command, and make all user modes global — P11 slice 254
Part A — +l locops user mode + LOCOPS command (charybdis UMODE_LOCOPS), the
local-server sibling of +z/OPERWALL: an oper broadcasts to +l users on the
originating server only; the command never crosses a link. Oper-gated to set,
letter 'l' (bit 0x40000). New command/locops.rs + golden_locops.rs +
locops_proptest.rs. The last named standard charybdis umode — surface complete.
Part B — leveva has NO local-only user modes (product decision). SEND_UMODES is
now every UserMode::ALL bit (self-maintaining const loop), so +O/+x/+s/+g/+G/+D/
+R/+Q/+z/+l all propagate and a remote WHOIS shows them. Verified safe: the burst
already rendered the full unmasked set, and a user's host is always carried
explicitly (UNICK field / ENCAP CHGHOST), never recomputed from the +x bit, so
+x cannot double-apply the cloak (+x now emits both UMODE +x and ENCAP CHGHOST).
Enforcement unchanged: caller-id (+g/+G) and +R stay gated on the target's home
server — the ACCEPT list lives only there and is not propagated, so those S2S
gates are the actual enforcement, not redundant re-checks, and are kept.
- mode.rs: UserMode::LocOps + is_locops(); doc sweep (bit propagates, enforcement
is server-local)
- command/mode.rs: 'l' in the oper-gated arm + umode_diff; +x test asserts both frames
- s2s/umode.rs: SEND_UMODES = every umode; exclusion test -> carries-every-mode
- snotice.rs / s2s/relay.rs: stale 'not in SEND_UMODES' comments corrected
- help/LOCOPS.md + USER_MODES +l row; 004 umode list -> ...zSl (6 snapshots)
- golden_s2s_umode rewritten (demonstrates +D now propagating)
feat(leveva): +s/+y SPY snomask producer — STATS-request spy notices — P11 slice 260
Wire the third producer-less snomask category opened by slice 257 (after `n`
NCHANGE in 258 and `x` EXTERNAL in 259). charybdis (`SNO_SPY`, letter `y`) fans a
notice to local `+s +y` opers whenever a client requests STATS from this server —
the "someone is poking at the server" spy.
- snotice.rs: pure builder `stats_spy_notice(letter, nick, user, host, server)` ->
`STATS <c> requested by <nick> (<user>@<host>) [<server>]` (the fuzz seam).
- snomask.rs: `SnoMask::spy()` accessor (the `y` bit, reserved since 257); doc-table
row flipped to its producer.
- command/stats.rs: fire at the `stats` chokepoint before answering, gated on a
selector char being present AND the requester resolving to a local registry
record (its user@host source; unresolved only in synthetic tests). Fires for any
selector (even unrecognized), skips bare STATS; no self-exclusion; local-only
([<server>] = ctx.name, STATS answered locally).
Tests (RED first): builder render+embed units; `spy()` letter round-trip; stats
units (happy +s+y delivery with numerics asserted unchanged, inverses: +s+k-only
and -s get nothing, bare STATS fans nothing, unrecognized selector still fans,
unclaimed caller fans nothing); fuzz tests/spy_snomask_proptest.rs (builder never
panics, scaffold + each field verbatim).
Plan: docs/superpowers/plans/2026-06-18-p11-slice260-spy-snomask.md
feat(leveva): +s/+b BOTS snomask producer — Excess-Flood flooder spy + HELP SNOMASK — P11 slice 265
Wire the eighth producer for a slice-257 reserved-but-empty snomask category
(charybdis SNO_BOTS, `b`). A registered client disconnected for Excess Flood
(the command-rate fakelag kill, slices 201-208) now fans a
`Flooder <nick>!<user>@<host> (Excess Flood)` spy notice to local `+s +b`
watchers at the registered flood-kill chokepoint. The pre-registration flood
guard (slice 203) deliberately does not fire it (no nick/identity, no charybdis
analogue). Local-only like every snomask producer; charybdis BOTS is mainly
message-target flood while leveva fans on the live command-rate kill it has —
faithful intent, not a byte copy. Only `d` DEBUG now lacks a producer.
Also ship a `HELP SNOMASK` reference page cataloguing every snomask category
and trim the `HELP USER_MODES` snomask paragraph to allude to it.
- snomask.rs: SnoMask::bots() accessor (BOTS bit, reserved since 257); doc-table
`b` row + producer_accessors test.
- snotice.rs: pure flood_notice(nick, user, host) builder (the fuzz seam).
- session.rs: server_notice_cat(ctx, bots(), flood_notice(...)) at the registered
Excess-Flood kill site; unit tests (fan to +s+b and ALL watchers, inverses for
+s+k-only / -s / modest burst).
- help/SNOMASK.md (new) + help/USER_MODES.md (trimmed) + SNOMASK in the help
TOPICS allowlist.
- tests/bots_snomask_proptest.rs: builder never panics, carries the scaffold,
embeds each field over arbitrary input.
Gate: cargo test -p leveva green (golden_privmsg failure was a load flake under
two contending suites — passes isolated in 0.5s); clippy clean; workspace build
0 warnings.
feat(leveva): burst empty +P permanent channel over S2S — stale-S2S-flag-allowlist fix — P11 slice 269
Closes slice 244's documented "Bursting empty permanent channels on
link" follow-on. A +P channel survives memberless so its topic/modes
persist, but a freshly-linked peer's NJOIN carries only the
empty-channel placeholder (.), so the receiver dropped the whole channel
block (CHANTS on an unknown channel was a no-op, the . NJOIN added
nobody, and the following MODE/TOPIC landed on a nonexistent channel).
Receiver-only fix: s2s::chants::apply_encap_chants now materializes the
empty channel from the burst CHANTS line via the new
Channels::create_permanent_from_burst, gated on +P (pure
channel::burst_creates_permanent, the inverse of destroy_when_empty) so
a leaky non-permanent empty channel is never created and a known channel
is never clobbered.
Also fixes a root-cause sub-bug surfaced while scoping: the S2S
burst/MODE parser s2s::mode::supported_flag carried a stale nmtipsO-only
flag-letter allowlist that silently dropped every leveva-native channel
flag (+C +c +S +z +T +g +r +Q +F +P +L) off a server burst/MODE, so
they never replicated across a link. Both the client and S2S parsers now
delegate to one canonical ChanMode::flag_from_char so they can never
drift again.
Tests (TDD + fuzz): golden_s2s_permanent (MODE->324 +Pkl, TOPIC->332,
inverse non-+P->403); unit tests in channel/chants/mode; permanent_proptest
extended (burst_creates_permanent totality+inverse, create_permanent_from_burst
gated/never-clobbers).
feat(leveva): enforce advertised TOPICLEN on TOPIC set — grapheme-cap truncation — P11 slice 270
leveva advertised TOPICLEN=255 in 005/ISUPPORT but command::topic recorded a
TOPIC set of any length verbatim — the cap was cosmetic. charybdis/the IRCnet
oracle enforce it by copying the new topic into a fixed char topic[TOPICLEN+1]
buffer (silent truncation). leveva now honours its own advertised cap.
- isupport::truncate_topic(text) -> &str: pure, returns the longest prefix that
is at most TOPICLEN grapheme clusters (UAX #29), whole when it fits. Mirrors the
LINELEN cut in Message::to_wire; a grapheme boundary is a char boundary, so the
result is always valid UTF-8.
- command::topic truncates once at the top of the set branch, so the echo, the
local-member fan-out, the stored topic, the later 332 query, and the S2S relay
all carry the same text. A topic arriving over S2S is trusted (its origin
already truncated it), exactly as charybdis trusts a remote TOPIC.
Divergences: grapheme cap not byte cap (leveva's uniform length unit, slice 121);
home-server enforcement only; truncate, not reject (no ERR_* for "topic too long").
Tests (RED first): topic_set_truncates_to_topiclen / _at_exactly_topiclen_is_untouched
/ _truncates_multibyte_on_a_grapheme_boundary; isupport truncate_topic_caps_and_
passes_through + fuzz truncate_topic_proptest (byte-prefix, <= cap, untouched-iff-fits,
idempotent over arbitrary input).
Gate: cargo test -p leveva green; cargo clippy -p leveva --tests clean; cargo build
--workspace 0 warnings.
feat(leveva): D-lines (DLINE/UNDLINE) — IP-level operator bans (P11 slice 284)
charybdis-style D-lines, the IP/CIDR sibling of the temporary K-line family
(slices 281–283): `DLINE <duration> <ip/cidr> [:reason]` bans a raw IP or CIDR
block, `UNDLINE <ip/cidr>` lifts it. Built as a near-clone of the K-line plane
keyed on a single IP/CIDR `mask` (matched with the CIDR-aware
`matching::host_component_matches`) instead of `user@host`.
- `dline.rs`: `DlineStore` mirrors `KlineStore` (Vec+Mutex, NOCASE dedup,
`find_active`, same `database {}` SQLite write-through + boot reload as slice
282); `Dline::covers` = `host_component_matches`. Reuses the kline duration
grammar/clock/clamp.
- `command/dline.rs`: `dline`/`undline` mirror `tkline`/`untkline` (461→481 gate,
empty/`*` mask → Incorrect format, no success reply); `reap_matching_ip` ejects
local matching clients by `orighost` (real connect IP), skipping remote +
`kline-exempt`. New `OperPrivilege::Dline` (bit 0x400000).
- `session.rs`: registration gate checked before the K-line gate, matching the
pre-cloak connect IP → 465+ERROR+REJ snomask, never counted/claimed.
- `s2s/dline.rs`+`forward.rs`+`burst.rs`: `ENCAP * DLINE`/`UNDLINE` propagation
(server-prefixed, remaining-seconds, slice-281 KLINE shape) + burst
re-assertion.
- `stats.rs`: `STATS d` → `250 RPL_STATSDLINE` via `dline_report`.
- `server.rs`: `ctx.dlines` opens against the same `database {}` file (persists
across restart); boot test + inverse extended.
- help `DLINE.md`/`UNDLINE.md` + COMMANDS allowlist.
Tests (TDD, inverse invariants, mandated fuzzing): unit+proptest per module;
`tests/golden_dline.rs` (real binary, `dline.kdl` fixture) and
`tests/dline_proptest.rs` (6 properties incl. model-lockstep registration gate
over `Session::feed`). leveva-native, no oracle differential.
`cargo test -p leveva` green (2530 lib); clippy clean; workspace 0 warnings.
feat(leveva): TESTLINE ban-match diagnostic (P11 slice 285)
TESTLINE <[nick!]user@host | ip/cidr | nick | #channel> — a charybdis-style
read-only operator command, the capstone of the ban family (RESV 272, TKLINE
281, DLINE 284). It reports which active ban (K/D-line or RESV) would match a
mask, laying/lifting nothing.
- Param gate (461) then a plain oper-bit gate (any oper, no per-ban privilege
→ 481), matching charybdis (TESTLINE needs IsOper with no specific flag).
- A pure probe() classifier strips an optional nick! then routes by mask shape:
user@host → D-line(host)-then-K-line; #channel → channel RESV; bare token →
D-line(as IP)-then-nick RESV. Reports the first hit, priority D > K > R.
- New numerics RplTestline=725 (<type> <minutes-left> <ban-mask> :<reason>; 0
minutes for a permanent RESV) and RplNotestline=726 (<mask> :No matches).
Expiry honoured for free via find_active.
- TESTLINE.md help + COMMANDS allowlist (bijection test).
leveva-native, no oracle (2.11 has no TESTLINE): single best match, no
I-line/auth-block arm, local-only (no S2S propagation, mirroring charybdis).
Tests (TDD, failing first + fuzzing): command/testline.rs units + inline
probe_is_total proptest; testline_proptest.rs (reply-well-formed, non-oper-481,
laid-ban-always-reported inverse); golden_testline.rs (real binary, minutes
field masked). cargo test -p leveva green (2543 lib), clippy clean, workspace
0 warnings.
feat(leveva): MASKTRACE mask-filtered extended trace (P11 slice 286)
Charybdis-style `MASKTRACE <nick!user@host mask> [<gecos mask>]` — the
mask-filtered 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/class_name, closing with 262 RPL_TRACEEND (no new numeric).
Leveva-native (IRCnet 2.11 has no MASKTRACE), local-only (no S2S, mirroring the
charybdis model). Emits 708 not charybdis's 709 (same divergence ETRACE
documents); class/ip/XLINE columns identical to ETRACE.
Tests (TDD, inverse invariants + fuzzing): 11 unit tests (gates, folded-order
sweep, full columns, and the inverses — non-matching host excluded, QUIT removes
the match, gecos filter excludes/re-includes, no-match is just the 262);
golden_masktrace.rs (real binary snapshot); masktrace_proptest.rs (4 properties
x 512 cases). Help page + COMMANDS allowlist entry.
Gate: cargo test -p leveva green; clippy clean; cargo build --workspace 0
warnings.
feat(leveva): CHANTRACE channel-scoped extended trace (P11 slice 287)
The channel-scoped sibling of TRACE/ETRACE/MASKTRACE, completing the trace
quartet: CHANTRACE <#channel> reports every member of a named channel in
ETRACE's extended 708 column format (folded-nick order), closing 262.
Gate (charybdis-faithful ordering): 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). Reuses
etrace::etrace_line, no new numeric.
Divergences (leveva-native, no oracle): emits 708 not charybdis 709; 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).
Tests (TDD, inverse invariants + fuzzing): 11 unit (403-before-442, PART
removes match, non-member admitted once joined, outsider never appears),
golden_chantrace (real binary), chantrace_proptest (4 properties, 512 cases).
feat(leveva): TESTMASK mask-population diagnostic (P11 slice 288)
TESTMASK <[nick!]user@host> [<gecos>] counts how many connected clients match
a hostmask (and optional realname glob), split into local vs remote — the
read-only population sibling of TESTLINE (slice 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) yields a NOTICE :Invalid parameters (faithful to charybdis's
sendto_one_notice, not a numeric); a registry sweep glob-matches
nick/user/host(+orighost)/gecos (nick & gecos default *) and tallies
is_local_uid into one 727 RPL_TESTMASKGECOS reply. New numeric 727 (charybdis's
724 is dead — no format string, no caller). Pure parse_mask fuzz seam.
Confirmed against cloned charybdis m_testmask.c / messages.h / numeric.h.
leveva-native, local-only (no S2S, mirroring charybdis).
Tests: 13 units (inverse filters + quit-drops-count), golden_testmask (real
binary), testmask_proptest (4 properties incl. l+g <= population, never panics).
feat(leveva): OLIST — operator LIST revealing +s/+p channels (P11 slice 293)
charybdis extensions/m_olist.c: the operator variant of LIST that bypasses the
secret (+s) / private (+p) channel-hiding so an oper enumerates every channel,
including the hidden ones a normal LIST skips. The read-only sibling of LIST and
the latest member of the charybdis operator diagnostic family
(TESTLINE/TESTMASK/MASKTRACE/CHANTRACE/FINDFORWARDS/OPME).
- Channels::list_all() (full folded sweep) + list_one_any(name) (named incl.
hidden) — the visibility-bypass seams, no filter; also the fuzz seam.
- command/olist.rs: plain oper-bit gate (non-oper 481 before any sweep), same
321/322/323 numerics as LIST; no-arg → all incl. +s/+p, named → that channel
revealed-or-skipped, no ELIST conditions (faithful to mo_olist).
- Wired "OLIST" + mod olist; help/OLIST.md + COMMANDS allowlist + index.md.
Divergences (leveva-native, no oracle): plain oper-bit gate (not a per-command
privilege), no +s audit snomask (read-only diagnostic family precedent),
local-only.
Tests: list_all/list_one_any units; 6 command units; olist_proptest (full-set
+ superset-of-list model equality, arbitrary_args_never_panic); golden_olist
(LIST hides #secret, OLIST reveals it, non-oper 481). Regenerating
golden_help_users also picks up the pre-existing stale operator-index line
(OPME/FINDFORWARDS from slices 291/292).
cargo test -p leveva green; clippy clean; build --workspace 0 warnings.
feat(leveva): CYCLE — self-only channel refresh (P11 slice 295)
charybdis-style `CYCLE <channel>` (extensions/m_cycle.c): part and rejoin a
channel *without races*. Faithful to the real m_cycle, the effect is entirely
client-side for the issuer — `:source PART <chan> :Cycling`, `:source JOIN
<chan>`, then a fresh 353/366 — and nothing changes server-side, so the user
keeps op/voice and place and cannot be locked out by +i/+k/+l or lose a glare.
No new Channels seam (CYCLE never mutates): it composes the read-only
display_name/is_member/names_query accessors. Per-channel gate ladder:
461 no param → 403 nonexistent → 442 not-a-member → success, existence before
membership; the re-sent NAMES honours the issuer's multi-prefix/userhost-in-names.
Divergences (leveva-native, no oracle): self-only/observationally-pure (NOT
member-visible — that would drop ops, destroy a sole-member channel, and desync
peers), fixed :Cycling reason, local-only.
7 command units + cycle_proptest (observational-purity + arbitrary-args) +
golden_cycle; CYCLE.md help + COMMANDS allowlist + index.md.
refactor: extract leveva-casemap leaf crate (+ proptest fuzzing)
Continue the leaf-crate carve-out (after leveva-message / leveva-patricia) by
moving the RFC 1459 case-folding module out of the monolith into its own
workspace crate:
- leveva-casemap: byte/slice fold, compare, hash, and the owned-key `fold`. It
imports only `core` (Ordering + Hasher) and is the lowest leaf in the string
cluster — string/ident/matching/channel/registry/whowas depend on it, it on
nothing. std-only lib, proptest dev-dep.
leveva re-exports it as leveva::casemap (`pub use leveva_casemap as casemap;`),
so every crate::casemap::* / leveva::casemap::* path across ~40 source and test
files is unchanged — no consumer churn.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching leveva-message. Added 7 consistency/inverse
invariants fed arbitrary bytes and arbitrary (incl. non-ASCII) strings:
fold ⇔ eq_ignore_case both directions, cmp==Equal ⇔ eq, cmp antisymmetry,
hash-agrees-with-eq (prefix-free 0xff marker exercised), fold idempotent +
UTF-8-safe + byte-length-preserving, high bytes (≥0x80) identity, and
to_upper∘to_lower identity on the uppercase domain over the full byte range.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests
clean; leveva-casemap 13 (6 unit + 7 fuzz) and the full leveva suite (60
binaries) green.
refactor: extract leveva-cidr leaf crate (+ proptest fuzzing)
Carve the pure CIDR network-mask helpers (parse_cidr / contains /
network_base / cidr_match) out of leveva into a self-contained
leveva-cidr leaf crate, re-exported as `leveva::cidr` — continuing the
post-P12 leaf-crate carve-out after casemap/string/message/patricia.
cidr is a genuinely pure leaf: zero workspace deps, only std::net. This
corrects the earlier leveva-string survey, which filed matching+cidr as
"mutually referential, not a clean single leaf yet" — cidr's references
to matching/connlimit are doc-comment-only; the real edge is one-way
matching -> cidr. Extracting cidr therefore *unblocks* matching, whose
only remaining intra-crate deps are now casemap + cidr (both crates).
git mv leveva/src/cidr.rs -> leveva-cidr/src/lib.rs verbatim except the
doctest import and two cross-crate doc links; leveva/src/lib.rs swaps
`pub mod cidr;` for `pub use leveva_cidr as cidr;`. All four call sites
use fully-qualified crate::cidr::*, so zero consumer churn.
Fuzzing (the user ask): 10 proptest invariants over the full v4/v6
address space and arbitrary in-range prefixes, all consistency/inverse
properties — parse round-trip + out-of-range reject, contains reflexive
through network_base, network_base idempotent, contains prefix-monotone,
/0 universal + full-length exact, family mismatch never contains, and
cidr_match == parse + contains.
Next extraction candidates: matching (now a clean leaf, the C match.c
cluster), cloak (zero intra-crate deps), uid (blocked on ident).
Gate: cargo build -p leveva 0 warnings; cargo clippy -p leveva-cidr
--tests clean; cargo test -p leveva-cidr 18 (8 unit + 10 fuzz) + 1
doctest green; leveva cidr-consumer tests (matching/connlimit/websocket,
54) green.
refactor: extract leveva-string leaf crate (+ proptest fuzzing)
Continue the leaf-crate carve-out (after leveva-message / leveva-patricia /
leveva-casemap) by moving the casemapping-aware IRC string types out of the
monolith into their own workspace crate:
- leveva-string: IrcStr / IrcString (str/String wrappers whose equality,
ordering, and hashing fold under RFC 1459) + IrcError + IrcStringBuilder. The
architectural #2 leaf in the string cluster — now that casemap is its own crate,
string depends on exactly one workspace crate (leveva-casemap) plus core/std,
while ident/channel/registry/whowas all depend on it. No cycle: leveva-message
uses neither, leveva-casemap depends only on core.
leveva re-exports it as leveva::string (`pub use leveva_string as string;`), and
the existing `pub use string::{IrcError, IrcStr, IrcString, IrcStringBuilder};`
re-exports straight out of the new crate — so every crate::string::* /
leveva::string::* path and the four flat re-exports are unchanged across all
consumers (the full dependent build confirms zero churn). Body moved verbatim
except `use crate::casemap;` -> `use leveva_casemap as casemap;` and the two
doctests' `use leveva::...;` -> `use leveva_string::...;`.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/message. Added 6 consistency/inverse
invariants fed arbitrary (incl. non-ASCII) strings: eq lifts casemap (the wrapper
adds no equality of its own), Ord consistent-with-Eq + antisymmetric, Hash agrees
with Eq (the HashMap-key contract behind the nick/channel tables), Borrow<IrcStr>
round-trip (insert under one casing, find by a case-different borrowed key),
validation soundness (accept iff no NUL/CR/LF, FromStr agrees, storage
byte-identical on success), and builder = deferred try_from.
What else can move out: cloak (zero intra-crate deps — perfect standalone leaf,
strongest next pick); uid (only crate::ident); matching+cidr are a coupled pair;
msgid pulls cap/clock/ident. Recorded in the plan + progress log.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-string 13 (7 unit + 6 fuzz) + 2 doctests green; full leveva suite (413
binaries) green.
refactor: extract leveva-cloak leaf crate (+ proptest fuzzing)
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr) by moving the +x hidden-host
transform out of the monolith into its own workspace crate:
- leveva-cloak: the deterministic FNV-keyed IP/host cloak (cloak / cloak_ip /
cloak_host), a faithful port of Elemental-IRCd's ip_cloaking.c. The cleanest
remaining leaf, exactly as the leveva-cidr survey predicted ("zero intra-crate
deps — perfect standalone leaf, strongest next pick"): it depends only on
std::net::IpAddr and on nothing in the workspace. Its only crate::-references
were doc comments — the actual code edges all run *into* it
(command::mode::apply_cloak_change at mode.rs:916 and session.rs:1786 call
crate::cloak::cloak), so no cycle.
leveva re-exports it as leveva::cloak (`pub use leveva_cloak as cloak;`), so every
crate::cloak::* / leveva::cloak::* path is unchanged across consumers. Body moved
verbatim via git mv — no doctest, and every intra-doc link (cloak/cloak_ip/
cloak_host/IP_CHARTABLE/HOST_ALPHABET + the FNV reference) resolves in-module, so
nothing repointed.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/string/cidr. And since the C 2.11 oracle has
no +x usermode, there is NO differential oracle here — the 10 proptest invariants
ARE the gate, not a supplement. Fed arbitrary (incl. non-ASCII) strings and the
full v4/v6 address space: determinism (the ban-on-a-cloak-keeps-biting contract),
the leading-colon wire-safety biconditional (a non-: input never grows a leading
colon, a :-input is returned verbatim), length preservation for both cloak_ip and
cloak_host, separator/dot byte-index invariance, IP-literal dispatch to cloak_ip,
the g-z IP alphabet (v4 keeps its first two octets; v6 is hex/:/g-z with no leading
colon), and fnv_hash_32 == charybdis's explicit shift-decomposition over arbitrary
bytes.
What else can move out: matching (now a clean casemap+cidr leaf, pulls nom — the C
match.c cluster, natural next pick); uid (only crate::ident, blocked on extracting
ident); msgid pulls cap/clock/ident. Recorded in the plan + progress log.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-cloak 18 (8 unit + 10 fuzz) green; leveva +x consumer tests (command::mode
plus_x suite + session registers-cloaked + the s2s +s+x spy, 8) green.
refactor: extract leveva-matching leaf crate (+ proptest fuzzing)
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr / leveva-cloak) by moving the IRC
glob matcher + nick!user@host hostmasks out of the monolith into its own
workspace crate:
- leveva-matching: the C match.c cluster (matches / Pattern / collapse / HostMask
/ host_component_matches / allow_host_matches, 498 LoC). The survey-flagged
"natural next pick": once leveva-cidr split, its only intra-crate edges were
casemap::to_lower and cidr::cidr_match — both already crates — plus nom. Deps =
leveva-casemap + leveva-cidr + nom.
Body moved verbatim via git mv; five repoints only: `use crate::casemap;` ->
`use leveva_casemap as casemap;`, `crate::cidr::cidr_match` ->
`leveva_cidr::cidr_match`, the module-header intra-doc link (crate::casemap) ->
(leveva_casemap), and two doctests `use leveva::matching::…` ->
`use leveva_matching::…`. leveva re-exports it as leveva::matching
(`pub use leveva_matching as matching;`), so the flat
`pub use matching::{collapse, matches, HostMask, Pattern};` and all ~13 consumers
(xline / kline / resv / webirc / metadata / registration +
command/{oper,map,mask,hunt,dline,tkline}) keep their crate::matching::* paths.
No dependency cycle — the code edges all run into matching.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching the other leaves. leveva's matcher deliberately
diverges from match.c (RFC 1459 case-fold, not ASCII tolowertab; #=any-digit), so
there is NO byte-faithful differential oracle — the 13 proptest invariants ARE the
gate. Fed arbitrary (incl. non-ASCII) strings + glob masks: totality/no-panic over
every entry point, `*` is universal, `?`^k iff exact byte-length, case-insensitivity
under leveva_casemap::fold of the name, compile == matches, collapse is
matching-preserving + idempotent + non-growing, HostMask re-parses its own Display,
matches_target == matches_parts, allow_host_matches decomposes into user-glob &&
host-component, and a glob host component falls back to matches.
Gate: cargo build --workspace (0 warnings) · cargo clippy --workspace --tests
(clean) · cargo test -p leveva-matching (28 unit/collapse/escaping + 13 fuzz, + 2
doctests) green · leveva matching-consumer tests (xline/kline/resv/webirc, 181) green.
What else can move out: uid (only crate::ident, blocked on extracting ident — ident
itself blocked by a single crate::command reference worth auditing, the next unlock);
msgid pulls cap/clock/ident; clock is a small candidate leaf. Recorded in the plan +
progress log.