From 4daf8fd7e86d4f0f8b8e9d747622c94b68f2c98b Mon Sep 17 00:00:00 2001 From: Aybook <265313202+Aybook@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:09:07 +0200 Subject: [PATCH] =?UTF-8?q?docs(plugin-api):=20make=20the=20=C2=A78=20expo?= =?UTF-8?q?rts=20table=20exhaustive=20(#457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §8 closed with "See each plugin's source for the full exported surface", so it read as a contract but was a 7-row sample. During #447 PR 6 that made "not in §8" look like evidence of a dead export and cost a full re-derivation. Re-derived the real surface from source: - 19 plugins return an export table; 15 have a public (bare-name) surface, 4 (cmd_gag, etc_blocklist, etc_whitelist, hub_runtime) export only `_`-prefixed unit-test / migration seams. - 4 of the 7 previously-listed rows were themselves incomplete (bot_opchat missing `bot`, cmd_ban missing `del`/`bans_path`, etc_hubcommands missing `has`/`list`, etc_report missing `broadcast`). - Live doc bug fixed: cmd_ban exports `bans` as a TABLE by reference (mutated in place, per the #238/#239 idiom), not the callable `ban.bans()` the old table showed. Verified against importers (cmd_accinfo, etc_prometheus). Documented `ban.bans` + `ban.bans_path`. Table is now exhaustive (15 rows), states the completeness contract, and documents the `_`-prefix convention, so absence from §8 provably means "exports nothing public". Signatures mirror each plugin's own advertised export contract (a few exports carry extra optional impl params the plugin author does not advertise - e.g. cmd_ban.add's 6th `permanent` arg is absent from cmd_ban's own export comment - and are deliberately left out of a quick-reference table). Docs-only, no code change. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 ++ docs/PLUGIN_API.md | 46 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a3090a..66789d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,8 @@ land on `release/3.1.x` per ### Documentation +- [#457](https://github.com/luadch-ng/luadch/issues/457) - **`docs/PLUGIN_API.md` §8 now enumerates every public plugin export instead of sampling 7 of the 15 that have a public surface.** The old table closed with "See each plugin's source for the full exported surface", so §8 read as a contract but was a sample - during [#447](https://github.com/luadch-ng/luadch/issues/447) PR 6 "not in §8" was mistaken for "dead export" and cost a full re-derivation. The table is now exhaustive (15 rows) and states the completeness contract plus the repo convention that `_`-prefixed exports are internal unit-test / migration seams, deliberately omitted - four plugins (`cmd_gag`, `etc_blocklist`, `etc_whitelist`, `hub_runtime`) export only such seams and appear nowhere, so absence now provably means "exports nothing public". Also fixes a live doc bug: `cmd_ban` exports `bans` as a **table by reference** (mutated in place, per the #238/#239 idiom), not the callable `ban.bans()` the old table showed; the new row documents `ban.bans` + the `ban.bans_path` load-on-demand alternative. Docs-only, no code change. 3.2.x only. + - **new [`CONTRIBUTING.md`](CONTRIBUTING.md) - a contributor entry point, closing a real doc gap.** Three external PRs ([#437](https://github.com/luadch-ng/luadch/pull/437) / [#438](https://github.com/luadch-ng/luadch/pull/438) / [#439](https://github.com/luadch-ng/luadch/pull/439)) all arrived against `master`, and correctly so from the outside: nothing documented the GitFlow-A branch policy, `master` is the repository's default branch (so GitHub offers it), and `README.md` literally described 3.2.x as "active development (`master`)" without mentioning `dev` at all. The new page leads with the branch table (`dev` = target this, `master` = release substrate, `release/3.1.x` = security backports only), records that a mistargeted PR just gets retargeted by a maintainer with the contributor's authorship intact (no close-and-reopen), routes vulnerability reports to [`docs/SECURITY.md`](docs/SECURITY.md), and links out for the rest - build, tests, the core restricted-env `use "X"` contract, the plugin sandbox, the companion scripts repo - rather than duplicating them; the engineering how-to stays in [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md). `README.md` gains a `dev`-branch pointer directly under the release-lines table (the sentence that was actively pointing contributors at the wrong branch) plus `CONTRIBUTING.md` and the previously-unlisted `docs/DEVELOPMENT.md` in its documentation list; `docs/DEVELOPMENT.md`'s orientation table gains a "your first PR" row. Docs-only, no code change. 3.2.x only. - Contributor-doc currency + pattern capture (post-#398 webhook arc). **`CLAUDE.md`:** latest-release marker v3.1.12 -> **v3.1.13**; the §3 module map gains `core/hmac.lua` (HMAC-SHA256, #398) and `core/geoip_update.lua` (was omitted); the §5 current-era note re-framed from "bug triage" to feature-work + triage; §8 gains a "drop-in plugin patch as a lighter alternative to a 3.1.x release" note (the `usr_uptime` v0.10.1 case). **`docs/DEVELOPMENT.md`:** six durable patterns added - `scope="none"` custom-auth HTTP routes, verifying a signed body with `hmac.sha256` + dynamically-named per-instance secret keys, bots-at-module-load / routes-in-`onStart` (reload safety), bulky operator config in its own `cfg/.tbl`, the `getpeername()`-nil-after-accept guard (#401), and poll-don't-fixed-settle for a smoke assertion whose hub state installs asynchronously and fails open (#408). **`docs/HTTP_API.md`:** the §4.4 `none` scope row and the §5.1 plugin-registration `scope` arg now document `scope="none"` for plugin-owned auth (`etc_webhook` is the first user); the `meta` key list gains `plugin`. Plus: two unit tests that existed but were never wired into CI (`etc_blocklist_test`, `cmd_usercleaner_orphan_test` - both pass) are now registered on both smoke legs, so `tests/README`'s "the whole set runs in CI" holds. Docs / test-wiring only. 3.2.x only. diff --git a/docs/PLUGIN_API.md b/docs/PLUGIN_API.md index a34ab70f..ba1354bc 100644 --- a/docs/PLUGIN_API.md +++ b/docs/PLUGIN_API.md @@ -766,19 +766,41 @@ end ) ## 8. Bundled plugin exports -Plugins commonly imported via `hub.import()`: - -| Plugin | Module | Methods | +Every bundled plugin with a public surface is listed here - this table +is exhaustive, not a sample. A plugin's public API is whatever its +final `return { ... }` exports under a **bare name**, reached via +`hub.import( "" )`. Exports whose name begins with `_` are +internal unit-test or migration seams (the repo convention for "not +API") and are omitted on purpose; four plugins - `cmd_gag`, +`etc_blocklist`, `etc_whitelist`, `hub_runtime` - export **only** such +seams and so appear nowhere below. Absence from this table therefore +means a plugin exports nothing public, not that the doc is behind. + +| Plugin | Import | Public exports | |---|---|---| -| `bot_opchat` | `opchat = hub.import("bot_opchat")` | `opchat.feed( msg )` - send a message to the opchat | -| `bot_regchat` | `regchat = hub.import("bot_regchat")` | `regchat.feed( msg )` - send to regchat | -| `etc_report` | `report = hub.import("etc_report")` | `report.send( activate, hubbot, opchat, level, msg )` | -| `cmd_ban` | `ban = hub.import("cmd_ban")` | `ban.add( user, target, bantime, reason, script )`, `ban.bans()` | -| `usr_uptime` | `uptime = hub.import("usr_uptime")` | `uptime.tbl()` returns user-uptime database | -| `etc_trafficmanager` | `block = hub.import("etc_trafficmanager")` | `block.add( firstnick, scriptname, reason )`, `block.del( firstnick, scriptname )` | -| `etc_hubcommands` | `hubcmd = hub.import("etc_hubcommands")` | `hubcmd.add( cmd_or_list, onbmsg_fn )` - register a `+command` handler | - -See each plugin's source for the full exported surface. +| `bot_opchat` | `opchat = hub.import( "bot_opchat" )` | `opchat.feed( msg )` - send a normal message to the opchat; `opchat.bot` - the opchat bot object | +| `bot_regchat` | `regchat = hub.import( "bot_regchat" )` | `regchat.feed( msg )` - send a normal message to the regchat | +| `cmd_ban` | `ban = hub.import( "cmd_ban" )` | `ban.add( user, target, bantime, reason, script )` (bantime in seconds); `ban.del( target )`; `ban.bans` - the in-memory ban table **by reference** (see note); `ban.bans_path` - the store path for load-on-demand | +| `cmd_help` | `help = hub.import( "cmd_help" )` | `help.reg( title, usage, desc, level )` - register a `+help` entry | +| `etc_aliases` | `al = hub.import( "etc_aliases" )` | `al.resolve( name )` -> alias target or nil; `al.get_aliases_tbl()` -> the alias table | +| `etc_blocklist_feeds` | `feeds = hub.import( "etc_blocklist_feeds" )` | `feeds.get_status()` -> per-feed refresh status | +| `etc_clientblocker` | `cb = hub.import( "etc_clientblocker" )` | `cb.resolve( version_string )` -> block reason or nil; `cb.get_patterns_tbl()` -> the pattern table | +| `etc_geoip` | `geo = hub.import( "etc_geoip" )` | `geo.resolve( country, asn )` -> match string or nil; `geo.classify( ip )` -> country, asn, org; `geo.get_status()` -> policy + DB status | +| `etc_hubcommands` | `hubcmd = hub.import( "etc_hubcommands" )` | `hubcmd.add( cmd_or_list, onbmsg_fn )` - register a `+command` handler; `hubcmd.has( cmd )` -> bool; `hubcmd.list()` -> `{ name, fn }` pairs | +| `etc_msgmanager` | `msg = hub.import( "etc_msgmanager" )` | `msg.get_block_tbl()` -> the message-block table | +| `etc_proxydetect` | `pd = hub.import( "etc_proxydetect" )` | `pd.classify( parsed, ip )` -> matched proxy/VPN types; `pd.get_status()` -> provider + cache status | +| `etc_report` | `report = hub.import( "etc_report" )` | `report.send( activate, hubbot, opchat, level, msg )` - report to ops at or above `level`; `report.broadcast( msg, llevel, ulevel, from, pm )` - broadcast to a level **range** | +| `etc_trafficmanager` | `block = hub.import( "etc_trafficmanager" )` | `block.add( firstnick, scriptname, reason )`; `block.del( firstnick, scriptname )` | +| `etc_usercommands` | `ucmd = hub.import( "etc_usercommands" )` | `ucmd.add( menu, command, params, flags, llevel )` - register a user command; `ucmd.format( menu, command, params, flags, llevel )` -> the UCMD string | +| `usr_uptime` | `uptime = hub.import( "usr_uptime" )` | `uptime.tbl()` -> the user-uptime database table | + +> `cmd_ban.bans` is the live table **by reference** - cmd_ban mutates +> it in place (never rebinds), so a captured `local bans_tbl = +> ban.bans` stays valid; if you rebind your own local instead, that +> local goes stale across `+reload` (the #238/#239 hazard, see +> [`§10`](#10-common-pitfalls)). When you only need a point-in-time +> read, `util.loadtable( ban.bans_path )` at the call site is the +> load-on-demand-safe alternative. ---