From 879706059dcb35d38dcd37c0545edc17a96d1d6a Mon Sep 17 00:00:00 2001 From: Ling Bao Date: Wed, 9 Sep 2026 13:48:25 +1000 Subject: [PATCH 1/4] docs(10-14): catalogue the eighteenth repository's feature and UAT surface Read all 5 src/main/java files in full and produced FEATURES.md and UAT-CHECKLIST.md, plus a new .github/pull_request_template.md adapted from UltiChat's merged template (corrected for this repository's own reality: CRLF line endings throughout, and zero .github/workflows at all rather than "holds only maven-ci.yml and publish.yml"). This repository is the phase's own live test of two rules: - Zero lines are written, not omitted: @ConfigEntity, @ConditionalOnConfig, and @ConfigEntry all read 0 against 0 with a stated reason (this example ships no configuration of its own, by design -- it demonstrates the External Plugin API from a plain Bukkit JavaPlugin). - A gate that did not run is reported as not having run, not silently passed: this repository has no .github/workflows directory at all, so gate 4 will be stated as "no configured CI" in the merge report, never claimed green. The persistence row (ultitools-example.data.persistence) is the only place in this phase's eighteen repositories where the External Plugin API's own DataOperator data path is exercised end to end on a real server. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NDELX2DH8xeytNqSmSgy6F --- .github/pull_request_template.md | 42 +++++++++ FEATURES.md | 142 +++++++++++++++++++++++++++++++ UAT-CHECKLIST.md | 78 +++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 FEATURES.md create mode 100644 UAT-CHECKLIST.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..095e422 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,42 @@ +## Summary + + + +## Issue closure + + + +## Verification + + + +## Checklist + + + +- [ ] Targets `master` +- [ ] Line endings preserved per file (`file ` before and after; this tree is CRLF) +- [ ] Every comment, javadoc, workflow comment, and this PR's own title and body are English-first with Chinese as a supplement, and nothing was added to `.github/cjk-allowlist.txt` +- [ ] `FEATURES.md` and `UAT-CHECKLIST.md` updated for every feature change in this PR, or N/A with the reason diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..9cc87da --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,142 @@ +# UltiTools External Plugin Example — Feature Inventory + +This document catalogues every operator- or player-visible function, command, content item and +configuration key in this repository, as read directly from source. It is an internal reference +for UAT execution and issue reconciliation — the public description of these features lives on +. Update this file in the same pull request as any feature change. + +This repository is not a plugin module in the usual sense — it is a worked, buildable example +demonstrating how a **plain Bukkit `JavaPlugin`** (one that does NOT extend `UltiToolsPlugin`) +integrates with the framework through the External Plugin API (`UltiToolsAPI.connect(this)`, +`UltiToolsAPI.getDataOperator(...)`, `UltiToolsAPI.getEventBus()`, +`UltiToolsAPI.disconnect(this)`). Its five `@CmdMapping` commands and one `@EventListener` class +exist specifically to exercise that API surface end to end, including the one place in the +eighteen repositories this phase covers where the External Plugin API's own `DataOperator` data +path is exercised on a real server. + +## Conventions + +- **ID grammar:** `..`, dot-separated, every segment lowercase ASCII + drawn from `[a-z0-9-]`. `` is the repository name lowercased with no separators; per + this phase's fixed exception, `UltiTools-External-Example`'s repo-slug is `ultitools-example` + (not the mechanically-derived `ultitoolsexternalexample`). `` is the feature section's + slug. `` is the verb. An ID changes only when the feature's identity changes, never on + rewording. IDs are unique within a repository. This repository has **no** `@ConfigEntity` + class, so it has no config-shaped ID exception to apply — the `config` row shape described + elsewhere in this convention (used by every other repository in this fan-out) has zero + instances here, by design (see `## Configuration` below). +- **Kind**, exactly these eight values: `command`, `config`, `event`, `gui`, `scheduled`, + `placeholder`, `persistence`, `gate`. Each maps one-to-one onto a reconciliation-table line. + This repository has no `config` rows (zero `@ConfigEntity` classes — this example ships no + configuration of its own, by design: it demonstrates the External Plugin API from a plain + Bukkit plugin, not a configuration surface), no `gui` rows (no GUI page class), no `scheduled` + rows (no `@Scheduled` method — this example does not demonstrate `TaskManager`), no + `placeholder` rows (no PlaceholderAPI expansion), and no `gate` rows (no `@ConditionalOnConfig` + site — there is no config to condition on) — five of the eight Kinds stay in the vocabulary for + cross-repository consistency even though they appear zero times below. +- **Tier**, exactly three: `player`, `admin`, `internal`. Judged from what the feature is for. + `hello` and `info` are harmless demonstrations any player could run. `visit`, `visitors`, and + `delvisitor` are explicitly labeled "Data Storage Tests" in the source's own code comment — they + exist to exercise `DataOperator` CRUD by hand, not to serve a real gameplay purpose, so they are + `internal`. All five share the same class-level permission node (`ultiext.greet`, undeclared in + `plugin.yml`, so OP-only by Bukkit's own default) — an ordinary non-OP player cannot currently + reach the `internal`-tier commands, so no D-11 issue is filed for them. +- **Manual**, exactly three: `detailed`, `brief`, `none`. This repository's own features are + developer-facing example code, not something doc.ultikits.com describes for end users — every + row below is `none`, since there is nothing here for the public manual to expand. +- **Target**, exactly four: `player`, `console`, `both`, or `n/a`. This repository's one + `@CmdExecutor` class carries `@CmdTarget(BOTH)` at the class level with no method-level + override, so all five command rows below carry Target `both`. +- **Permission:** the literal node string, `none`, or `n/a`. This repository's one `@CmdExecutor` + class does not set `requireOp = true`, so no row below carries that suffix; all five commands + rely on the single literal node `ultiext.greet` alone. No permission node is declared in + `plugin.yml` (no `permissions:` section exists), so Bukkit's own undeclared-permission default + applies: OP-only. +- **Source:** `ClassName#member` — the class and member that actually implements the feature. +- **Row order:** by section, then by ID ascending within the section. +- **No manual prose:** no troubleshooting column, no explanatory paragraphs, no draft page text. + +### Reconciliation command family + +The canonical form for counting an annotation site across this repository's real sources: + +```bash +find -path '*/src/main/java/*' -name '*.java' -not -path '*/target/*' \ + -not -path '*/.worktrees/*' -print0 | xargs -0 grep -nE '^[[:space:]]*@AnnotationName\b' | wc -l +``` + +This repository is a single-root Maven project (`src/main/java` only, 5 source files total), +carries no git worktree directory, and has no javadoc or string-literal mention of any of its own +annotation names — the naive (unanchored) and line-start counts are identical for every kind +measured below, but the anchored `find`/`grep` form is used regardless, so the same command is +trustworthy unmodified against every repository in the fan-out. + +**Positive controls**, each confirmed by reading the cited line directly, not by trusting the +count alone: + +| Annotation | Sites | Positive control | +|---|---|---| +| `@CmdExecutor` | 1 | `GreetCommand.java:21`, class-level, `alias = {"ultiext", "uext"}` | +| `@CmdMapping` | 5 | `GreetCommand.java:31` (`hello`), `:37` (`info`), `:44` (`visit `), `:74` (`visitors`), `:93` (`delvisitor `) | +| `@EventListener` | 1 | `JoinListener.java:16`, class-level, on `PlayerJoinEvent`'s handler class | +| `@Scheduled` | 0 | no background task exists anywhere in this repository's 5 source files — confirmed by reading all 5 in full | +| `@ConfigEntity` | 0 | this repository ships no configuration of its own, by design — it demonstrates the External Plugin API from a plain Bukkit plugin, not a configuration surface. Zero written, not omitted: this line is the phase's own live test of that rule | +| `@ConditionalOnConfig` | 0 | there is no configuration key to condition registration on — direct consequence of the `@ConfigEntity` line above | +| `@ConfigEntry` | 0 | same reason as `@ConfigEntity` — zero keys, because zero config classes | +| `@Table` | 1 | `VisitorRecord.java:13`, `@Table("visitor_records")` | + +This document's command-row count (5) matches the `@CmdMapping` site count exactly (5 against 5). +The three `@ConfigEntity`/`@ConditionalOnConfig`/`@ConfigEntry` lines above all read 0 against 0 +with a stated reason — this repository is the phase's own live instance of the rule that a zero +count is written, never silently omitted from the reconciliation table. + +## Greeting + +`GreetCommand` — class-level `@CmdExecutor(permission = "ultiext.greet", alias = {"ultiext", +"uext"})`, `@CmdTarget(BOTH)`. Backed by `GreetService`, an `@Service` bean injected via +`@Autowired`, demonstrating that this framework's IoC container auto-scans and wires beans for a +plain `JavaPlugin` connected through `UltiToolsAPI.connect(this)`, exactly as it does for a real +`UltiToolsPlugin` module. + +| ID | Feature | Kind | How to reach | Permission | Target | Tier | Manual | Source | +|---|---|---|---|---|---|---|---|---| +| ultitools-example.greet.hello | Send a fixed greeting naming the sender (or `Console` for a non-player sender), proving `@Service`/`@Autowired` injection works for an externally-connected plugin | command | `/ultiext hello` (alias `/uext hello`) | ultiext.greet | both | player | none | GreetCommand#hello, GreetService#greet | +| ultitools-example.greet.info | Show a fixed plugin-info line naming this example and confirming `@Service` injection succeeded | command | `/ultiext info` (alias `/uext info`) | ultiext.greet | both | player | none | GreetCommand#info, GreetService#info | + +## Data Storage (External Plugin API) + +`GreetCommand`'s three data-manipulation sub-commands, explicitly labeled "Data Storage Tests" in +this class's own code comment — they exist to exercise `UltiToolsAPI.getDataOperator(this, +VisitorRecord.class)`'s CRUD surface by hand, not to serve real gameplay. `VisitorRecord` +(`@Table("visitor_records")`, extends `BaseDataEntity`, current-generation API, not the +deprecated `AbstractDataEntity`) is this repository's one persisted entity, tracking a player +name, a visit count, and a last-visit timestamp. + +| ID | Feature | Kind | How to reach | Permission | Target | Tier | Manual | Source | +|---|---|---|---|---|---|---|---|---| +| ultitools-example.data.delvisitor | Delete a visitor record by player name; reports success unconditionally regardless of whether a matching record actually existed — `DataOperator#del` is not checked for a prior existence, so deleting a name with no record produces the identical "[DATA] Deleted..." message as deleting one that did exist | command | `/ultiext delvisitor ` | ultiext.greet | both | internal | none | GreetCommand#delVisitor | +| ultitools-example.data.visit | Record a visit for the named player: creates a new `VisitorRecord` (visit count 1) if none exists for that name, or increments the existing record's visit count and updates its `last_visit` timestamp otherwise | command | `/ultiext visit ` | ultiext.greet | both | internal | none | GreetCommand#visit | +| ultitools-example.data.visitors | List every currently stored visitor record with its player name and visit count, or a "no records" message if the table is empty | command | `/ultiext visitors` | ultiext.greet | both | internal | none | GreetCommand#visitors | +| ultitools-example.data.persistence | Every `VisitorRecord` created or updated through `/ultiext visit` or an automatic on-join visit (see `## Player Join` below) survives a full server restart — `UltiToolsAPI.getDataOperator` resolves a `DataOperator` backed by the framework's own configured storage backend (JSON/SQLite/MySQL per the framework's `config.yml` `datasource.type`), scoped to this plugin's own data folder (or a `DataScope` if the connecting adapter supplies one), the same persistence guarantee any `UltiToolsPlugin` module's own `@Table` entities get — this is the only place in this phase's eighteen repositories where the External Plugin API's own data path, rather than a module's built-in one, is exercised end to end on a real server | persistence | run `/ultiext visit `, then restart the server, then run `/ultiext visitors` | n/a | n/a | internal | none | VisitorRecord#VisitorRecord, UltiToolsExtExample#onEnable, ExternalPluginAdapter#getDataFolder | + +## Player Join + +`JoinListener` — `@EventListener` on a plain Bukkit `Listener` implementation, demonstrating that +the framework auto-registers a `@EventListener`-annotated class's Bukkit `@EventHandler` methods +for a plain `JavaPlugin` connected via `UltiToolsAPI.connect(this)`. Bundles two independently +observable behaviours in one handler: a chat greeting, and an automatic visit-record +creation/increment identical in effect to running `/ultiext visit ` by hand. + +| ID | Feature | Kind | How to reach | Permission | Target | Tier | Manual | Source | +|---|---|---|---|---|---|---|---|---| +| ultitools-example.join.on-join | Send the joining player the same fixed greeting `/ultiext hello` produces, and automatically create or increment that player's `VisitorRecord` exactly as `/ultiext visit ` would, without the player running any command | event | join the server as any player | n/a | n/a | player | none | JoinListener#onJoin | + +## Configuration + +This repository has **zero** `@ConfigEntity` classes, **zero** `@ConfigEntry` keys, and **zero** +`@ConditionalOnConfig` sites — by design, not by omission. It demonstrates the External Plugin +API from a plain Bukkit `JavaPlugin`, and ships no configuration surface of its own; its +`plugin.yml` carries only the fixed fields Bukkit itself requires (`name`, `version`, `main`, +`api-version`, `description`, `authors`, `depend`), none of which is operator-configurable content +in the sense this document otherwise catalogues. This section intentionally carries no rows — see +the reconciliation table above for the explicit 0-against-0 lines this absence produces. diff --git a/UAT-CHECKLIST.md b/UAT-CHECKLIST.md new file mode 100644 index 0000000..e88b139 --- /dev/null +++ b/UAT-CHECKLIST.md @@ -0,0 +1,78 @@ +# UltiTools External Plugin Example — UAT Checklist + +This document is the executable companion to `FEATURES.md`: one row per feature stating the +steps to exercise it and the observable truth that proves it works. It is an internal reference +for real-machine verification, not user-facing documentation. + +> Batches are dispatched at 60 rows or fewer, and a batch never spans two repositories. There are +> exactly two legitimate exits to `human-uat-pending`: a row needing the pixel layer while the +> real-client harness is not ready, and a row needing personal credentials. Every other row must +> reach `pass`, `fail`, or `blocked`. + +## Conventions + +- **Columns:** `ID`, `Preconditions`, `Steps`, `Expected`, `Layer`, `Covers`. +- **ID:** cites its `FEATURES.md` ID verbatim. A negative case suffixes the checklist ID only, + as `.neg-` — a negative case still tests the same feature, so the base ID is unchanged. +- **Layer**, copied verbatim from Laojun's own `ultitools-real-client-uat` skill so no + translation step exists at dispatch time: `protocol`, `java-client`, `os-input`, `pixel`, + `server`, `human`. +- **Human-authenticated-session rows (D-27b):** a row whose Steps can only be exercised through + the maintainer's own authenticated UltiCloud panel session carries the fixed Preconditions + phrase `maintainer-authenticated UltiCloud panel session (personal credentials)` and Layer + `human`, ending at `human-uat-pending` by design. This repository has no panel-capability rows + of its own (it is a plain external Bukkit plugin with no UltiCloud/panel integration at all — + that surface belongs to `UltiToolsPlugin` modules, not to a plugin connected only through the + External Plugin API), so no row below is currently affected; the convention is stated here for + template consistency with the framework's checklist. +- **Expected** must name an observable truth — an exact chat line, a log line, a database row, + an inventory slot — and never the words "it works". +- **Covers** back-references a Phase 9 GUI-excluded class name; left blank when no such class + applies. This repository is not one of the nine modules in Phase 9's GUI-exclusion register + (confirmed by reading + `.planning/phases/09-module-ecosystem-readiness-and-test-coverage/gui-exclusions/` — no file + named for this repository exists there), so every row below leaves `Covers` blank. This + repository also has no GUI page class of its own (`FEATURES.md`'s own `gui`-Kind count is 0), + so the question does not arise a second way either. +- A row whose Preconditions name a prior row must appear after that row in file order — asserted + mechanically: for every row, every checklist ID cited in its Preconditions cell must have a + strictly smaller line number in this file than the row citing it (sweep class 8, D-27a). +- **Config-per-file rule (D-06):** one checklist row per `@ConfigEntity`-annotated class or per + shipped yml file, never one row per key. This repository ships zero `@ConfigEntity` classes and + zero configuration yml files of its own (`plugin.yml` carries only Bukkit's own required + plugin-descriptor fields, not operator-configurable content) — zero config-per-file rows exist + below, and this is correct, not a missing section: see `FEATURES.md`'s `## Configuration` + section for the same rule stated on the catalogue side. +- This repository's `plugin.yml` declares no `permissions:` section, and none of its five + `@CmdMapping` sub-commands has an in-game i18n catalogue to check against (this example has no + `lang/*.json` file at all — every player-facing string is a hardcoded English literal in + `GreetCommand.java`/`GreetService.java`/`JoinListener.java`), so no `language: en` precondition + is needed on any row below, unlike a module that ships its own `lang/zh.json`/`lang/en.json` + pair with a non-English shipped default. + +## Greeting + +| ID | Preconditions | Steps | Expected | Layer | Covers | +|---|---|---|---|---|---| +| ultitools-example.greet.hello | none | Run `/ultiext hello` as a player named `Tester` | Chat line reads exactly `Hello Tester! This message is from an external plugin using UltiTools-API.` | server | | +| ultitools-example.greet.hello.neg-console | none | Run `/ultiext hello` from the server console | Console line reads exactly `Hello Console! This message is from an external plugin using UltiTools-API.` — `GreetCommand#hello` substitutes the literal string `Console` for any non-`Player` sender, proving `@CmdTarget(BOTH)` genuinely admits console execution rather than merely declaring it | server | | +| ultitools-example.greet.info | none | Run `/ultiext info` | Chat/console line reads exactly `UltiTools External Example v1.0.0 - verifying @Service injection works!` | server | | + +## Data Storage (External Plugin API) + +| ID | Preconditions | Steps | Expected | Layer | Covers | +|---|---|---|---|---|---| +| ultitools-example.data.delvisitor | a visitor record named `probe` exists — run `/ultiext visit probe` first if none does | Run `/ultiext delvisitor probe`, then run `/ultiext visitors` | Chat/console line reads `[DATA] Deleted visitor record for probe`; the subsequent `/ultiext visitors` no longer lists `probe` | server | | +| ultitools-example.data.delvisitor.neg-not-found | no visitor record named `ghost` exists (delete it first via `ultitools-example.data.delvisitor` if a prior dispatch created one, or use a name never visited in this session) | Run `/ultiext delvisitor ghost` | Chat/console line STILL reads `[DATA] Deleted visitor record for ghost` — `GreetCommand#delVisitor` calls `DataOperator#del` unconditionally and reports success regardless of whether a matching row ever existed; this is the row's actual assertion, not a bug workaround | server | | +| ultitools-example.data.visit | no visitor record named `probe` currently exists (run `ultitools-example.data.delvisitor` first if one does) | Run `/ultiext visit probe` | Chat/console line reads `[DATA] Created record for probe — first visit!` | server | | +| ultitools-example.data.visit.neg-repeat | a visitor record named `probe` exists with visit count 1 (run `ultitools-example.data.visit` immediately before this row, in the same dispatch) | Run `/ultiext visit probe` a second time | Chat/console line reads `[DATA] Updated probe — visit #2` — the SAME record's count incremented, not a duplicate row created | server | | +| ultitools-example.data.visitors | at least one visitor record exists (run `ultitools-example.data.visit` first) | Run `/ultiext visitors` | Chat/console starts with `[DATA] === Visitor Records () ===` where `` matches the actual stored count, followed by one ` — visits` line per record | server | | +| ultitools-example.data.visitors.neg-empty | zero visitor records exist (delete every record created earlier in this dispatch via `ultitools-example.data.delvisitor`) | Run `/ultiext visitors` | Chat/console shows exactly `[DATA] No visitor records.` — no header line, no record lines | server | | +| ultitools-example.data.persistence | a visitor record named `restarttest` exists with visit count 1 (run `ultitools-example.data.visit` with `restarttest` first) | Stop the server completely (a full clean shutdown, not `/ul reload`), start it again, then run `/ultiext visitors` | `restarttest` is still listed with visit count 1 — `UltiToolsAPI.getDataOperator`'s backing store persisted the record to disk (JSON/SQLite/MySQL per the framework's own `datasource.type`), independent of this plugin's own in-memory state, which is rebuilt from scratch on every `onEnable` | server | | + +## Player Join + +| ID | Preconditions | Steps | Expected | Layer | Covers | +|---|---|---|---|---|---| +| ultitools-example.join.on-join | no visitor record exists yet for the joining player's name | The player joins the server for the first time in this session | The player receives the chat line `Hello ! This message is from an external plugin using UltiTools-API.` (identical to `/ultiext hello`'s own line, substituting the real player name), and a subsequent `/ultiext visitors` (run by any sender holding `ultiext.greet`) lists that player's name with visit count 1, without the player having run any command themselves | server | | +| ultitools-example.join.on-join.neg-rejoin | a visitor record already exists for the joining player's name, with visit count `N` (the player joined once already earlier in this same dispatch, per `ultitools-example.join.on-join`) | The same player quits and rejoins | The player again receives the join greeting; a subsequent `/ultiext visitors` shows that player's visit count as `N + 1`, NOT reset to 1 and NOT a second entry for the same name — `JoinListener#onJoin` looks up the existing record by name and increments it, mirroring `/ultiext visit`'s own repeat-visit behaviour | server | | From 45a60c70402b64bf0fc1c5d0eea69b62590ccde4 Mon Sep 17 00:00:00 2001 From: Ling Bao Date: Wed, 9 Sep 2026 15:57:47 +1000 Subject: [PATCH 2/4] fix(10-14): state the OP/permission precondition every greet sub-command needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugin.yml declares no permissions: section, and all five GreetCommand sub-commands share the single class-level permission node ultiext.greet — so Bukkit's own undeclared-node default (OP-only) gates every one of them. Every row whose Steps dispatch through a non-console player now states that precondition explicitly; the console row (hello.neg-console) is unaffected since Bukkit's console sender always passes permission checks. Also added a Conventions note stating the systemic cause once, matching the existing per-row-plus-Conventions pattern this document already uses for the reload/restart precondition. Codex review round 1 on commit 8797060 flagged this as a class-4 unconstrained-target defect on the hello row specifically; the same defect applies to every other player-dispatched row in this document, so all are fixed together in this one commit per D-24/D-26. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NDELX2DH8xeytNqSmSgy6F --- UAT-CHECKLIST.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/UAT-CHECKLIST.md b/UAT-CHECKLIST.md index e88b139..6a2a30e 100644 --- a/UAT-CHECKLIST.md +++ b/UAT-CHECKLIST.md @@ -49,26 +49,35 @@ for real-machine verification, not user-facing documentation. `GreetCommand.java`/`GreetService.java`/`JoinListener.java`), so no `language: en` precondition is needed on any row below, unlike a module that ships its own `lang/zh.json`/`lang/en.json` pair with a non-English shipped default. +- **The same absent `permissions:` section also means every one of the five sub-commands is + OP-only by Bukkit's own undeclared-node default**, since all five share the single class-level + `@CmdExecutor(permission = "ultiext.greet")` node. Every row below whose Steps dispatch through + a non-console player therefore states that player must be OP or hold `ultiext.greet`, stated in + each row's own Preconditions cell rather than only here — an ordinary non-OP player dispatching + any of these commands is rejected before the command body ever runs, which a Precondition that + merely says "none" would let an executor discover only as a false failure. The one row that + dispatches from the server console (`ultitools-example.greet.hello.neg-console`) is unaffected: + Bukkit's console `CommandSender` always passes every permission check. ## Greeting | ID | Preconditions | Steps | Expected | Layer | Covers | |---|---|---|---|---|---| -| ultitools-example.greet.hello | none | Run `/ultiext hello` as a player named `Tester` | Chat line reads exactly `Hello Tester! This message is from an external plugin using UltiTools-API.` | server | | +| ultitools-example.greet.hello | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only | Run `/ultiext hello` as a player named `Tester` | Chat line reads exactly `Hello Tester! This message is from an external plugin using UltiTools-API.` | server | | | ultitools-example.greet.hello.neg-console | none | Run `/ultiext hello` from the server console | Console line reads exactly `Hello Console! This message is from an external plugin using UltiTools-API.` — `GreetCommand#hello` substitutes the literal string `Console` for any non-`Player` sender, proving `@CmdTarget(BOTH)` genuinely admits console execution rather than merely declaring it | server | | -| ultitools-example.greet.info | none | Run `/ultiext info` | Chat/console line reads exactly `UltiTools External Example v1.0.0 - verifying @Service injection works!` | server | | +| ultitools-example.greet.info | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only | Run `/ultiext info` as a player | Chat/console line reads exactly `UltiTools External Example v1.0.0 - verifying @Service injection works!` | server | | ## Data Storage (External Plugin API) | ID | Preconditions | Steps | Expected | Layer | Covers | |---|---|---|---|---|---| -| ultitools-example.data.delvisitor | a visitor record named `probe` exists — run `/ultiext visit probe` first if none does | Run `/ultiext delvisitor probe`, then run `/ultiext visitors` | Chat/console line reads `[DATA] Deleted visitor record for probe`; the subsequent `/ultiext visitors` no longer lists `probe` | server | | -| ultitools-example.data.delvisitor.neg-not-found | no visitor record named `ghost` exists (delete it first via `ultitools-example.data.delvisitor` if a prior dispatch created one, or use a name never visited in this session) | Run `/ultiext delvisitor ghost` | Chat/console line STILL reads `[DATA] Deleted visitor record for ghost` — `GreetCommand#delVisitor` calls `DataOperator#del` unconditionally and reports success regardless of whether a matching row ever existed; this is the row's actual assertion, not a bug workaround | server | | -| ultitools-example.data.visit | no visitor record named `probe` currently exists (run `ultitools-example.data.delvisitor` first if one does) | Run `/ultiext visit probe` | Chat/console line reads `[DATA] Created record for probe — first visit!` | server | | -| ultitools-example.data.visit.neg-repeat | a visitor record named `probe` exists with visit count 1 (run `ultitools-example.data.visit` immediately before this row, in the same dispatch) | Run `/ultiext visit probe` a second time | Chat/console line reads `[DATA] Updated probe — visit #2` — the SAME record's count incremented, not a duplicate row created | server | | -| ultitools-example.data.visitors | at least one visitor record exists (run `ultitools-example.data.visit` first) | Run `/ultiext visitors` | Chat/console starts with `[DATA] === Visitor Records () ===` where `` matches the actual stored count, followed by one ` — visits` line per record | server | | -| ultitools-example.data.visitors.neg-empty | zero visitor records exist (delete every record created earlier in this dispatch via `ultitools-example.data.delvisitor`) | Run `/ultiext visitors` | Chat/console shows exactly `[DATA] No visitor records.` — no header line, no record lines | server | | -| ultitools-example.data.persistence | a visitor record named `restarttest` exists with visit count 1 (run `ultitools-example.data.visit` with `restarttest` first) | Stop the server completely (a full clean shutdown, not `/ul reload`), start it again, then run `/ultiext visitors` | `restarttest` is still listed with visit count 1 — `UltiToolsAPI.getDataOperator`'s backing store persisted the record to disk (JSON/SQLite/MySQL per the framework's own `datasource.type`), independent of this plugin's own in-memory state, which is rebuilt from scratch on every `onEnable` | server | | +| ultitools-example.data.delvisitor | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `probe` exists — run `/ultiext visit probe` first if none does | Run `/ultiext delvisitor probe` as a player, then run `/ultiext visitors` | Chat/console line reads `[DATA] Deleted visitor record for probe`; the subsequent `/ultiext visitors` no longer lists `probe` | server | | +| ultitools-example.data.delvisitor.neg-not-found | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; no visitor record named `ghost` exists (delete it first via `ultitools-example.data.delvisitor` if a prior dispatch created one, or use a name never visited in this session) | Run `/ultiext delvisitor ghost` as a player | Chat/console line STILL reads `[DATA] Deleted visitor record for ghost` — `GreetCommand#delVisitor` calls `DataOperator#del` unconditionally and reports success regardless of whether a matching row ever existed; this is the row's actual assertion, not a bug workaround | server | | +| ultitools-example.data.visit | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; no visitor record named `probe` currently exists (run `ultitools-example.data.delvisitor` first if one does) | Run `/ultiext visit probe` as a player | Chat/console line reads `[DATA] Created record for probe — first visit!` | server | | +| ultitools-example.data.visit.neg-repeat | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `probe` exists with visit count 1 (run `ultitools-example.data.visit` immediately before this row, in the same dispatch) | Run `/ultiext visit probe` as a player a second time | Chat/console line reads `[DATA] Updated probe — visit #2` — the SAME record's count incremented, not a duplicate row created | server | | +| ultitools-example.data.visitors | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; at least one visitor record exists (run `ultitools-example.data.visit` first) | Run `/ultiext visitors` as a player | Chat/console starts with `[DATA] === Visitor Records () ===` where `` matches the actual stored count, followed by one ` — visits` line per record | server | | +| ultitools-example.data.visitors.neg-empty | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; zero visitor records exist (delete every record created earlier in this dispatch via `ultitools-example.data.delvisitor`) | Run `/ultiext visitors` as a player | Chat/console shows exactly `[DATA] No visitor records.` — no header line, no record lines | server | | +| ultitools-example.data.persistence | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `restarttest` exists with visit count 1 (run `ultitools-example.data.visit` with `restarttest` first) | Stop the server completely (a full clean shutdown, not `/ul reload`), start it again, then run `/ultiext visitors` as a player | `restarttest` is still listed with visit count 1 — `UltiToolsAPI.getDataOperator`'s backing store persisted the record to disk (JSON/SQLite/MySQL per the framework's own `datasource.type`), independent of this plugin's own in-memory state, which is rebuilt from scratch on every `onEnable` | server | | ## Player Join From d9d05a9982442085ab0be6b6a8f0ad4536f0235f Mon Sep 17 00:00:00 2001 From: Ling Bao Date: Wed, 9 Sep 2026 16:05:42 +1000 Subject: [PATCH 3/4] fix(10-14): correct the getEventBus claim, and harden two UAT rows against false failures - FEATURES.md's intro overstated the External Plugin API surface this example demonstrates: it claimed UltiToolsAPI.getEventBus() usage, but none of the 5 source files call it (confirmed by full read plus a repository-wide grep for EventBus, zero hits). JoinListener's @EventListener reaches ListenerManager#registerAllExternal, which calls Bukkit's own PluginManager#registerEvents directly -- a distinct mechanism from the module EventBus this example does not exercise. Corrected the claim and named the real registration path. - ultitools-example.data.visit.neg-repeat's Expected asserted "not a duplicate row created" with no Step that could observe a duplicate -- a regression inserting a second probe record alongside the updated one would still pass on the success-message check alone. Added a /ultiext visitors query and an exactly-once assertion. - ultitools-example.data.persistence's post-restart query had no constraint on the querying player's own name; if that player is itself named restarttest, JoinListener#onJoin auto-increments the very record under test before the query runs, producing a false persistence failure. Preconditions and Steps now require a different player (or the console) for the query. Second Codex review round on commit 45a60c7 found all three; all are wrong-verdict per D-24/D-26 (a literal mismatch and two unconstrained targets), fixed together in this one commit. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NDELX2DH8xeytNqSmSgy6F --- FEATURES.md | 17 ++++++++++++----- UAT-CHECKLIST.md | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 9cc87da..c5aea69 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -8,11 +8,18 @@ for UAT execution and issue reconciliation — the public description of these f This repository is not a plugin module in the usual sense — it is a worked, buildable example demonstrating how a **plain Bukkit `JavaPlugin`** (one that does NOT extend `UltiToolsPlugin`) integrates with the framework through the External Plugin API (`UltiToolsAPI.connect(this)`, -`UltiToolsAPI.getDataOperator(...)`, `UltiToolsAPI.getEventBus()`, -`UltiToolsAPI.disconnect(this)`). Its five `@CmdMapping` commands and one `@EventListener` class -exist specifically to exercise that API surface end to end, including the one place in the -eighteen repositories this phase covers where the External Plugin API's own `DataOperator` data -path is exercised on a real server. +`UltiToolsAPI.getDataOperator(...)`, `UltiToolsAPI.disconnect(this)`) and through the framework's +own annotation-driven registration (`@Service`/`@Autowired` bean wiring, `@CmdMapping` command +dispatch, `@EventListener` Bukkit event registration). Its five `@CmdMapping` commands and one +`@EventListener` class exist specifically to exercise that surface end to end, including the one +place in the eighteen repositories this phase covers where the External Plugin API's own +`DataOperator` data path is exercised on a real server. `UltiToolsAPI.getEventBus()` — the +framework's separate cross-module pub/sub system — is a real method on the same API class, but +this example does not call it anywhere in its 5 source files (confirmed by reading all 5 in full +and by a repository-wide grep for `EventBus`, zero hits); `JoinListener`'s `@EventListener` +annotation instead reaches `ListenerManager#registerAllExternal`, which calls Bukkit's own +`PluginManager#registerEvents` directly — a distinct mechanism from the module EventBus, and not +demonstrated by this repository. ## Conventions diff --git a/UAT-CHECKLIST.md b/UAT-CHECKLIST.md index 6a2a30e..27a668b 100644 --- a/UAT-CHECKLIST.md +++ b/UAT-CHECKLIST.md @@ -74,10 +74,10 @@ for real-machine verification, not user-facing documentation. | ultitools-example.data.delvisitor | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `probe` exists — run `/ultiext visit probe` first if none does | Run `/ultiext delvisitor probe` as a player, then run `/ultiext visitors` | Chat/console line reads `[DATA] Deleted visitor record for probe`; the subsequent `/ultiext visitors` no longer lists `probe` | server | | | ultitools-example.data.delvisitor.neg-not-found | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; no visitor record named `ghost` exists (delete it first via `ultitools-example.data.delvisitor` if a prior dispatch created one, or use a name never visited in this session) | Run `/ultiext delvisitor ghost` as a player | Chat/console line STILL reads `[DATA] Deleted visitor record for ghost` — `GreetCommand#delVisitor` calls `DataOperator#del` unconditionally and reports success regardless of whether a matching row ever existed; this is the row's actual assertion, not a bug workaround | server | | | ultitools-example.data.visit | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; no visitor record named `probe` currently exists (run `ultitools-example.data.delvisitor` first if one does) | Run `/ultiext visit probe` as a player | Chat/console line reads `[DATA] Created record for probe — first visit!` | server | | -| ultitools-example.data.visit.neg-repeat | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `probe` exists with visit count 1 (run `ultitools-example.data.visit` immediately before this row, in the same dispatch) | Run `/ultiext visit probe` as a player a second time | Chat/console line reads `[DATA] Updated probe — visit #2` — the SAME record's count incremented, not a duplicate row created | server | | +| ultitools-example.data.visit.neg-repeat | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `probe` exists with visit count 1 (run `ultitools-example.data.visit` immediately before this row, in the same dispatch) | Run `/ultiext visit probe` as a player a second time, then run `/ultiext visitors` | The first command's chat/console line reads `[DATA] Updated probe — visit #2`; the subsequent `/ultiext visitors` lists `probe` EXACTLY ONCE with `2 visits` — proving the SAME record's count incremented rather than a second, duplicate `probe` row being inserted alongside the first | server | | | ultitools-example.data.visitors | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; at least one visitor record exists (run `ultitools-example.data.visit` first) | Run `/ultiext visitors` as a player | Chat/console starts with `[DATA] === Visitor Records () ===` where `` matches the actual stored count, followed by one ` — visits` line per record | server | | | ultitools-example.data.visitors.neg-empty | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; zero visitor records exist (delete every record created earlier in this dispatch via `ultitools-example.data.delvisitor`) | Run `/ultiext visitors` as a player | Chat/console shows exactly `[DATA] No visitor records.` — no header line, no record lines | server | | -| ultitools-example.data.persistence | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `restarttest` exists with visit count 1 (run `ultitools-example.data.visit` with `restarttest` first) | Stop the server completely (a full clean shutdown, not `/ul reload`), start it again, then run `/ultiext visitors` as a player | `restarttest` is still listed with visit count 1 — `UltiToolsAPI.getDataOperator`'s backing store persisted the record to disk (JSON/SQLite/MySQL per the framework's own `datasource.type`), independent of this plugin's own in-memory state, which is rebuilt from scratch on every `onEnable` | server | | +| ultitools-example.data.persistence | the executing player is OP, or has been granted `ultiext.greet` — this plugin declares no `permissions:` section in `plugin.yml`, so Bukkit's own undeclared-node default is OP-only; a visitor record named `restarttest` exists with visit count 1 (run `ultitools-example.data.visit` with `restarttest` first); the player who runs the post-restart query below is named something OTHER than `restarttest` (or the query is run from the server console) — `JoinListener#onJoin` auto-increments a visitor record matching the JOINING player's own name, so a player named `restarttest` rejoining after the restart would increment the very record this row checks BEFORE `/ultiext visitors` ever runs, producing a false persistence failure | Stop the server completely (a full clean shutdown, not `/ul reload`), start it again, then run `/ultiext visitors` as a player NOT named `restarttest` (or from the console) | `restarttest` is still listed with visit count 1, unchanged by the restart or by any other player's own join — `UltiToolsAPI.getDataOperator`'s backing store persisted the record to disk (JSON/SQLite/MySQL per the framework's own `datasource.type`), independent of this plugin's own in-memory state, which is rebuilt from scratch on every `onEnable` | server | | ## Player Join From 86a8755900106f5d29849dff2cba48292bf35f42 Mon Sep 17 00:00:00 2001 From: Ling Bao Date: Wed, 9 Sep 2026 16:12:02 +1000 Subject: [PATCH 4/4] fix(10-14): describe the tree's mixed line-ending convention accurately The template's own checklist line asserted "this tree is CRLF", but the three files this PR adds (this template, FEATURES.md, UAT-CHECKLIST.md) are all LF while pre-existing tracked files (README.md, pom.xml, plugin.yml) are CRLF. A future contributor reading the blanket claim could wrongly convert the new LF documents to match it. Restated the convention as mixed, by file type, matching the same per-file preservation rule the checklist item already states. Third Codex review round on commit d9d05a9 found this; fixed per D-24. No fourth round requested: this is the second post-sweep round (per the dispatch facts' framing), and the finding is fixed in this one commit. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01NDELX2DH8xeytNqSmSgy6F --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 095e422..45c4568 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -37,6 +37,6 @@ Anything inside an HTML comment, a code fence, or backticks does not count as a - [ ] Targets `master` -- [ ] Line endings preserved per file (`file ` before and after; this tree is CRLF) +- [ ] Line endings preserved per file (`file ` before and after; this tree MIXES conventions -- pre-existing tracked files such as `README.md`, `pom.xml` and `src/main/resources/plugin.yml` are CRLF, while `FEATURES.md`, `UAT-CHECKLIST.md` and this template are LF -- match whichever convention the specific file you are touching already uses, never convert it) - [ ] Every comment, javadoc, workflow comment, and this PR's own title and body are English-first with Chinese as a supplement, and nothing was added to `.github/cjk-allowlist.txt` - [ ] `FEATURES.md` and `UAT-CHECKLIST.md` updated for every feature change in this PR, or N/A with the reason