12bd24a973d42359b6078d4ff59834f256cfe011
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 12bd24a973 |
docs(teams): queue the integration kit as phase 11, last before the cutover
The kit is the instruction book for putting a different game on this platform, written for an audience outside this org. Teams expands the contract that book teaches against, so the book is the last thing the bet owes before `edge` becomes `main` (org lead, 2026-08-18). One sentence in it is already wrong rather than merely incomplete. `book/02-website-module.md` tells a reader that core declares a slot and a module may only fill one. Phase 3 inverted exactly that, and by phase 6 module-uo declares three — a new game's module cannot implement Teams at all without the inverted direction. Two shapes are genuinely new and worth teaching: the inverted slot, and `registerTeamProvider` as the first registration where core calls the module and waits — with the asymmetry that every call fails stale except `projectRoster`, which fails closed, because for a visibility question "keep what you have" means serving the roster unprojected. The phase explicitly does NOT enumerate the contract. The kit already teaches four members and has never mentioned notification streams, announce legs or post hooks, all of which predate Teams. MODULE_API.md is normative; the kit teaches one path and links out. Its ordering is awkward and is stated rather than smoothed over: it is written before the cutover and can only merge after it, because CI clones the pinned sha and checks the template against that core's MODULE_API_VERSION — and 1.6.0 does not reach `main` until the cutover lands. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 5e284d5468 |
docs(teams): phase 6 as built — four deviations and a ninth contract member
Part 6 gains an as-built header rather than a rewrite, so the reasoning that produced the original design stays legible beside what the build learned. Four deviations. There was no web notification settings screen to add the Team list to — `/auth/me/notifications/*` was built for the app in M7 and had zero web consumers, which is survivable for push and not for a sink whose whole argument is the web-only user. Email defaults to `off` rather than `digest`, on the org lead's call: digest-by-default would start mailing every member of every Team the moment an operator connects Gmail. Roster events tickle but do not email. And a ninth member joined MODULE_API 1.6.0. `pageUrlTemplate` is the member, and it exists because phase 3 left core with no Team page and therefore no way to link to one. It joins 1.6.0 in place under the rule set in phase 2 — a contract owes a bump only once it has landed on `main`, and 1.6.0 has only ever been on `edge`. Two further build decisions are recorded where they belong: the digest computes at send time and keeps no queue (§6.4), and one-click unsubscribe is a stateless HMAC whose whole capability is muting one (user, Team) pair (§6.4). BACKEND_DESIGN gains the table, the two `/auth/me` routes and the unsubscribe endpoint — the only write in the public tier and the only route with no `siteMode`, because the mail went out before the site went into maintenance. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| aed4ec4166 |
docs(teams): phase 5 — discussion, the edit window, and reports that route around leadership
TEAMS.md §5.4, §5.5.7 (new), §5.6 and the Part 12 phase entry; BACKEND_DESIGN.md's schema and admin route tables. **The largest change is a decision, not a description.** §5.6's first rule said "a leader may also see and act on reports for their own Team, but staff always receive them". The org lead settled on 2026-08-18 that the leader half is **decided against, not deferred**: the gap the section exists to close is that leaders moderate their own forum and a Team's leaders are exactly the people who will not report their own Team, so a leader-visible queue hands a complaint about a leader back to them — and a read-only leader view still tells them who reported what. Recorded as an amendment rather than by editing the sentence away, because the reasoning for the original is what makes the correction legible. **§5.6's `content_reports` DDL does not work as written, and the amendment says so rather than quietly swapping it.** With `status` in the unique key, CLOSED rows collide with each other: dismiss a report, let the behaviour recur, dismiss the second one, and the UPDATE lands on a tuple that already exists — so the queue starts throwing duplicate-key errors on the first repeat reporter. The shipped table keys on a generated `open_marker`, the same encoding `team_forum_grants.active_marker` uses. Three smaller departures are recorded beside it: `handled_note`, the two username snapshots §2.10 asks for everywhere else, and a real CASCADE on `team_id`. **New §5.5.7 for `teams_forum_edit_window_minutes`** (0–1440, default 15), and the rule under it: the window is resolved on the server TWICE — the read path stamps `canEdit`/`editableUntil` so a client knows whether to draw the control, the write re-derives it from `created_at` before allowing anything. The read is advice and the write is enforcement, because a time-bounded permission must not take its clock from the party it bounds. That is also why the key is not published: the client needing the number is the admin screen, and the client needing the decision already has it per post. **§5.4 gains three notes its route table does not carry**: thread creation splits authority by TYPE rather than widening the leader gate (and reports it as two booleans, since one would make a client guess which right it described); post moderation is its own route whose validator accepts all eight actions so the model can say "pin applies to a thread, not to a post"; and a reply's three refusal codes are chosen to be distinguishable — 404 absent, 400 announcement, 409 locked — with locked refusing staff too. The Part 12 entry records what the phase disproved, its four acceptance criteria, that it spans ONE repo where phase 4 needed two, and the single defect the live rig found. It also notes that phase 4 shipped `uploads` with the default off, so §5.6's "pull reports forward if uploads is enabled anywhere" never triggered. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 0e665078e8 |
docs(teams): phase 4 — the forum's access model, switches and image policy
Records what building phase 4 settled, and what it disproved. The structural correction first: TEAMS.md 3.1 gave the forum a CORE page and phase 3 deleted every core Team page. The ROUTES were unaffected — they are all /player and /admin — but the participant surface had no home, and 5.4's route table did not notice. Settled the way phase 3 settled the activity feed: module-uo declares a second place on its guild page and core fills it, so the phase spans two repos rather than the one the plan named. Two slots rather than one, because a slot holds one component and the first fill wins; the panel navigates by search param because a thread must be linkable and core cannot mount a route on a page it does not own. Two findings from the sanitiser worth not re-deriving: `rel` has to be on the allowlist for the transform that WRITES it to survive, or every forum link ships without noopener; and the bare-URL linkifier runs after sanitising, over escaped text only, which is the property that makes it safe rather than an injection point. Also recorded: the upload sweep runs regardless of the current image mode, which is the mechanism behind the dialog's promise that disabling uploads does not delete what is already there; the two routes the table lacked; and the org lead's decision that all three proposed acknowledgement additions ship. BACKEND_DESIGN gains the four forum tables and the reasoning a reader of the schema alone would miss — why the guard is at the route and never at the data, why no stored body ever contains an <img>, what `uploads` mode hardens, and what the acknowledgement actually records. MODULE_API's inverted-slot section gains the rule a module needs: one slot per PLACE, not one per page. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| d78cc99c80 |
docs(teams): Teams is a contract, not a surface — record the correction
The org lead's correction to Part 3, and the inverted extension-slot direction it forces. TEAMS.md: §3.1's routes, §3.4's two slots and §3.5's three nav entries are all marked superseded in place, and Part 12's phase 3 entry gains the amendment explaining why — core does not own the word for a Team, so the module that owns the vocabulary owns the page. The five corrections found by building are kept alongside it. MODULE_API.md: 1.6.0's list swaps the two client slots for registry.declareModuleSlot + Slot in the UI kit, and a new §3.7a documents the inverted direction: what forced it, the enforced namespace, why core's fills are applied at mount rather than eagerly, and why a fill for an undeclared slot is a no-op where §3.7's unknown slot throws. BACKEND_DESIGN.md: the by-external-id lookup route. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| fd9c02130c |
docs(teams): Team pages and the activity feed, and what phase 3 disproved
TEAMS.md gains a dated amendment on phase 3 with five corrections, all found by
building the thing it describes:
- §3.2 and §3.4 contradict each other about `team.member.row`'s props, and
§3.2 wins because it is the security rule. A client slot can only receive
what the browser was sent, so §3.4's `{ memberKey, userId, displayName }`
means publishing both identifiers in every public roster, module installed
or not. The slot is redeclared with what core can honestly supply.
- §3.3's projection is an EIGHTH MODULE_API member where 1.6.0 listed seven.
Settled by the org lead: 1.6.0 is amended in place, on the rule Protocol 4
was given in phase 2 — a contract owes a bump only once it has reached
`main`.
- "the module declines" needed splitting in two before it could be built. No
module at all withholds nothing and must serve the roster whole; a module
whose rungs could not be consulted must serve none of it. Only the second
fails closed, or bare core shows an empty roster on every Team page.
- the module answers with member KEYS, not rows, so it can narrow what is
published and cannot widen it.
- core's five activity kinds are four until the forum lands, and a Team's
FIRST roster emits no join items at all.
§2.11's route table gains the activity endpoint it never had, and MODULE_API.md
documents `projectRoster`, the inverted fail-closed semantics that make it
different from every other provider call, and `ctx.teams.activity.push`'s item
shape and its four contractual properties.
BACKEND_DESIGN.md: the seventh Team table, its retention, and the three public
routes' new behaviour — `enabled` on the index, the slot props on the single
Team, the per-caller row projection on the roster, and the feed.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 680a4866ac |
docs(teams): Team core, MODULE_API 1.6.0, and what phase 2 disproved
Documents phase 2 of docs/website/TEAMS.md across the three files that had to
change, and records the five places building it disagreed with the design.
## MODULE_API.md — 1.6.0
The Team surface becomes contract: `api.registerTeamProvider(...)`,
`ctx.teams.publish` / `ctx.teams.reconcile` / `ctx.teams.activity.push`,
`api.registerSlashCommands(...)`, and the two client slots. Additions only, so
minor; module-uo's `coreApi: "^1.3.0"` still resolves.
Per the org lead's decision, one 1.6.0 covers the whole surface rather than a
minor per phase -- so the document names the phase against each member, and the
two that cannot work yet are marked as present-and-throwing rather than left to
be discovered at runtime.
`registerTeamProvider` gets the fullest treatment because it is the first
registration where core calls the MODULE and waits for an answer. The envelope,
the 10-second budget and the refusal semantics are all contract, not
implementation: they are how a module says "I cannot answer" without core hearing
"there is nothing". `ctx.teams` is documented as push-only, with the reason there
is no reader — a module answers questions about Teams, it does not ask them.
## BACKEND_DESIGN.md
The six Team tables, the rename rule, the active-only uniqueness encoding, the
per-column account-deletion decisions, and all eighteen routes across the three
tier tables.
Two entries there exist to stop a future reader "fixing" them: why
`team_forum_grants` does not use the obvious generated column, and why the two
columns TEAMS.md never mentioned have to exist.
## TEAMS.md — five amendments, marked as amendments with their date
- **§2.5's SQL and §2.10's decision cannot both hold.** MariaDB refuses ON
DELETE SET NULL on a base column of a stored generated column (1901), so
§2.5's `active_user` forces the CASCADE that §2.10 exists to prevent. §2.10
wins; the marker is re-encoded for identical semantics.
- **`team_forum_grants` lands in phase 2**, so the four-path resolver is written
once and its non-contamination tests are real.
- **Two columns the document did not contemplate**, both serving §2.4's gates:
`roster_synced_at`, because sync state is per MODULE and gate 3 leaves one
Team behind while the others sync; and `members_empty_since`, gate 4's
per-Team quarantine.
- **`leader` on the member shape is not path 2.** Taking §2.3 and §2.5 both
literally gives one column two writers, and the roster writes first — so a
refused `getTeamLeaders()` silently demoted everyone. Found by its own test.
- **§2.8.2's matcher needed two narrow widenings**, both real impersonation
vectors the whole-word rule missed: a term matches a name word's singular
("Guild of Moderators"), and a run of single-letter words is compared joined
("G.M."). Neither re-admits substring matching.
Pairs with website (Teams phase 2) and Module-uo (the provider).
Refs docs/website/TEAMS.md Part 12 phase 2
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| b369e728c2 |
docs(link): protocol 4 — guild membership on the wire
Adds v4.md as the spec of record for `guild.roster` and `guild.leave`, and corrects the two older documents that Protocol 4 makes wrong. PROTOCOL_2.md §10.1 already described this design — hold a member-serial set, diff it each sweep, emit join/leave — and 2.0 then shipped only the half needing no new state, folding membership into the board signature as a serial *sum*. The section has read ever since as though the whole thing were built. It now says which half shipped, and carries the correction that doing it produced: a sum is not a safe stand-in for a set, because one member joining and another leaving between two sweeps offset each other and the guild reads as unchanged. INTEGRATION.md gains both kinds in the event catalogue, the `roster` key on GET /guilds, and the three things an integrator gets wrong otherwise — that `guild.leave`'s `who` is a bare serial rather than an actor object (the mobile has already left, so there is nothing to attribute), that `acct` is genuinely optional on a member, and that a guild with no `roster` key is not the same as one with an empty roster. v4.md documents what the phase found as well as what it built: the missing store migration and the user_version decision, why the roster lives in its own column rather than inside the guild.update snapshot, why a split roster is reassembled in memory rather than appended to the column, and why guild.leave gets no board projection at all. §6 records that the reassembly bug was invisible to every unit test — they all exercised single-frame rosters — and only the live rig caught it. TEAMS.md is amended where this phase disagreed with it: Phase 1 spans five repos, not four, because installer/backup.rs justifies skipping the sidecar database on reasoning the migration falsifies. The user_version decision is recorded there too, since the design of record did not contemplate a migration mechanism at all. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 6f8b722acf |
docs(teams): use the correct presentation of the project name, and match both
"Runic Gateway" (two words) is the correct presentation; "RunicGateway" is accepted only where the name has to condense to a single token — the Gitea org, a package name, a URL segment. Three prose uses in TEAMS.md had the condensed form for no reason, including the upload warning text, and are corrected. The `RunicGateway/<repo>` slugs throughout android/PLAN.md are condensed by necessity and are left alone. This turned out to matter beyond presentation. The reserved-name matcher (2.8) specified whole-word matching over a normalisation that case-folds, strips punctuation and collapses whitespace — under which "RunicGateway" is a SINGLE word and would never have matched the two-word term "Runic Gateway". The condensed form is the one an impersonator reaches for, precisely because it is what the org and every URL already use, so the check would have missed its most likely input. Multi-word terms are now additionally compared with whitespace removed on both sides, so `Runic Gateway` matches `RunicGateway`, `runic-gateway`, `Runic_Gateway` and `RUNIC GATEWAY` alike. The widening applies only to terms CONTAINING whitespace, which keeps it clear of the single-word terms where whole-word matching is doing the false-positive work: `admin` is still compared as a word and still does not fire on "Badminton". Listing the condensed form as a separate reserved term was the alternative and was rejected — it is a second thing to keep in sync and would still miss the hyphenated and underscored variants. The same reasoning applies to a deployment's own BRAND_NAME, which is free text and may well be spaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz |
|||
| f944a89660 |
docs(teams): replace the draft upload warning with the org lead's wording
The drafted text was a placeholder the doc explicitly flagged as needing the org lead's ownership. Replaced with the supplied wording, which is better in three ways: it is factual rather than legalistic, it enumerates the specific responsibilities being accepted (moderation, storage and backups, legal compliance, community policy) instead of gesturing at them, and it states plainly that RunicGateway provides no hosted storage or content moderation services. Recorded as two surfaces rather than one, because they behave differently: a settings help text that is always on screen and explains the setting, and a confirmation dialog shown only when changing the mode to uploads, which is what the acknowledgement actually records. The dialog carries two checkboxes and the API still takes one `acknowledge: 1`. Recording two booleans would add nothing — there is no reachable state where an operator agreed to one clause and not the other and proceeded — while the stored version is what answers the question that matters later: which text did they agree to? Three additions are proposed on top and marked as droppable, since none is liability language and none changes what is being agreed to: that uploads are attributed and staff-removable (the reason the attribution table exists), that disabling uploads later does NOT delete files already uploaded, and that anyone with forum access can upload — including manually granted accounts with no linked game identity. Also adds the advisory `remote` mode needs, which the upload wording correctly does not cover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz |
|||
| 111b412fd5 |
docs(teams): design of record for platform Teams & community integration
Teams become a core platform primitive rather than a mechanism for generating Discord infrastructure: a module stays authoritative for who a Team is and who belongs to it, and core owns everything the platform attaches to it — pages, roster, activity, forums, notifications and optional external integrations. Investigated against the working tree rather than the brief, which corrected three of its premises: - guild.update carries member/online COUNTS, not a roster, and one leader. Every membership-derived feature has no data source today, so a protocol change (PROTOCOL_VERSION 3 -> 4, guild.roster + guild.leave) is the gating phase. - There is no module->core news hook to generalise. ctx.posts is read-only and both registerPostHook and registerAnnounceLeg run core->module, so the activity feed needs its own ingestion member modelled on ctx.push.publish. - Core has no SSE at all; it left with the module cutover. Live roster status is module-projected through an extension slot rather than a new core transport. Also settled: the four-path permission model (game membership / leadership / forum access / external access) with the non-contamination invariant; envelope- returning provider methods so module unavailability is structurally staleness and never an authoritative empty; reserved-name screening with auto-hide and an admin-approval gate on the three actions that publish untrusted game-sourced strings; forum admin controls with a renderer-owned image path; abuse reporting; email as a third notification sink; and a capability-based integration contract rather than a shared interface, with the Matrix research behind that choice. Part 10 sorts every artifact into contract / core-internal / module-owned / wire protocol, because almost none of this is contract: the surface is ten members, and the ~15 tables are core-internal and off limits to a module even though the module is what populates them. Proposes MODULE_API_VERSION 1.6.0 (minor, additions only) and an 11-phase plan. Design only — nothing is implemented, and MODULE_API.md remains normative. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnDSWzpUjw8t8C2hghysNz |