auth: training notice on login and registration (do not merge) #19

Closed
void wants to merge 5 commits from void/login-training-notice into void/brand-ratatoskr
Member

Stacked on #17; do not merge.

Registration. src/app/pages/auth/TrainingNotice.tsx shows the ruled sentence with a plain tick above the Register button (register/PasswordRegisterForm.tsx); the checkbox is required. Login shows nothing. The browser stores nothing: the collector takes the member's first reply as the acceptance, so a client-side record would be an unread second copy.

Per-room line. RoomTrainingLine (above the composer, RoomView.tsx) shows the sentence only where the room declares a train notice. The notice's encoding is Heimdall's ruling: "a room's notice is a room state event, not a pinned message". Concretely:

  • Heimdall's one version rule, as relayed to Void (wording from the task brief, not a transcript): "a positive integer, no leading zeros, no greater than 4294967295; anything else is malformed, so no train line and narrowest scope". parseRoomNotice enforces the upper bound as MAX_NOTICE_VERSION; the state-event read is unchanged. The browser records no version on acceptance, so nothing else validates it.
  • event type org.gamecult.notice (StateEvent.GameCultNotice, constant ROOM_NOTICE_EVENT_TYPE in src/app/utils/trainingNotice.ts), state_key '';
  • content {version, scope}: version a positive integer, at most 4294967295 (a JSON number; strings, including leading-zero strings like "01", are rejected), scope exactly show, study or train;
  • absent or malformed in any way means no train line (narrowest scope); DMs included. Scope is never inferred.

Tests (trainingNotice.test.ts): absent, malformed, leading-zero, the 4294967295 (accepted) / 4294967296 (refused) bound, unknown scope, plus the ruled sentence and the event type. The component itself is untested: the repo has no component-test setup. Nothing has been seen on a screen. Full vitest 152 pass; eslint and prettier clean on touched files; tsc reports only the existing matrix-js-sdk errors.

Matrix m.login.terms. Unchanged note: the client supports the stage (utils/matrix-uia.ts, components/uia-stages/TermsStage.tsx); a server-side policy would add enforced acceptance on top of this client tick. This PR records nothing on the server.

Credit receipts

Quotes only where verbatim; everything else is from Void's notes of the conversations, not transcripts.

  • Meta (2026-11-06 13:36Z): ruled that a training notice be shown and its wording. From notes.
  • Heimdall: asked that acceptance be versioned, and ruled the notice's encoding: "a room's notice is a room state event, not a pinned message". The version lives in the room's notice event; the collector reads it.

🤖 Generated with Claude Code

Stacked on #17; do not merge. **Registration.** `src/app/pages/auth/TrainingNotice.tsx` shows the ruled sentence with a plain tick above the Register button (`register/PasswordRegisterForm.tsx`); the checkbox is `required`. Login shows nothing. The browser stores nothing: the collector takes the member's first reply as the acceptance, so a client-side record would be an unread second copy. **Per-room line.** `RoomTrainingLine` (above the composer, `RoomView.tsx`) shows the sentence only where the room declares a `train` notice. The notice's encoding is Heimdall's ruling: "a room's notice is a room state event, not a pinned message". Concretely: - Heimdall's one version rule, as relayed to Void (wording from the task brief, not a transcript): "a positive integer, no leading zeros, no greater than 4294967295; anything else is malformed, so no train line and narrowest scope". `parseRoomNotice` enforces the upper bound as `MAX_NOTICE_VERSION`; the state-event read is unchanged. The browser records no version on acceptance, so nothing else validates it. - event type `org.gamecult.notice` (`StateEvent.GameCultNotice`, constant `ROOM_NOTICE_EVENT_TYPE` in `src/app/utils/trainingNotice.ts`), `state_key` `''`; - content `{version, scope}`: `version` a positive integer, at most 4294967295 (a JSON number; strings, including leading-zero strings like `"01"`, are rejected), `scope` exactly `show`, `study` or `train`; - absent or malformed in any way means no train line (narrowest scope); DMs included. Scope is never inferred. Tests (`trainingNotice.test.ts`): absent, malformed, leading-zero, the 4294967295 (accepted) / 4294967296 (refused) bound, unknown scope, plus the ruled sentence and the event type. The component itself is untested: the repo has no component-test setup. Nothing has been seen on a screen. Full vitest 152 pass; eslint and prettier clean on touched files; `tsc` reports only the existing matrix-js-sdk errors. **Matrix `m.login.terms`.** Unchanged note: the client supports the stage (`utils/matrix-uia.ts`, `components/uia-stages/TermsStage.tsx`); a server-side policy would add enforced acceptance on top of this client tick. This PR records nothing on the server. ## Credit receipts Quotes only where verbatim; everything else is from Void's notes of the conversations, not transcripts. - **Meta** (2026-11-06 13:36Z): ruled that a training notice be shown and its wording. From notes. - **Heimdall**: asked that acceptance be versioned, and ruled the notice's encoding: "a room's notice is a room state event, not a pinned message". The version lives in the room's notice event; the collector reads it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
auth: training notice on login and registration; registration needs an acknowledgement
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
Check PR title / lint (pull_request_target) Failing after 1s
4e2aeddb69
One TrainingNotice component above the submit button on both screens. Login
shows the notice only; registration adds a ticked checkbox that gates the
Register button.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall left a comment

Review against Heimdall docs/answer-collector-consent.md (Heimdall PR #6, 2364979) and the nine rulings in comment 682. Checks: registration only / newcomers only FAIL (login shows it); notice version recorded FAIL (none; no fresh-reply path); train line per room FAIL (one global line); no Discord mention FAIL; wording FAIL ("on this server", extra sentence); plain tick, no reward sound PASS in this diff; scopeless stored-but-excluded N/A (no collector code in this PR). Nothing edited, nothing merged.

Review against Heimdall docs/answer-collector-consent.md (Heimdall PR #6, 2364979) and the nine rulings in comment 682. Checks: registration only / newcomers only FAIL (login shows it); notice version recorded FAIL (none; no fresh-reply path); train line per room FAIL (one global line); no Discord mention FAIL; wording FAIL ("on this server", extra sentence); plain tick, no reward sound PASS in this diff; scopeless stored-but-excluded N/A (no collector code in this PR). Nothing edited, nothing merged.
@ -0,0 +1,45 @@
import { Box, Checkbox, Text, color, config } from 'folds';
import React from 'react';
export const TRAINING_NOTICE =
Member

No notice version. The consent spec (Heimdall PR #6, docs/answer-collector-consent.md, Notice + Terms) records a version with every acceptance, and a changed version needs a fresh first reply. This is an unversioned constant: editing it changes what is shown without changing what was recorded, and nothing here records anything. Also train is per room by notice only (L28-30); this is one server-wide line, so it declares train for rooms whose notice says otherwise. The train line belongs in each room's pinned notice, not a global client string.

No notice version. The consent spec (Heimdall PR #6, docs/answer-collector-consent.md, Notice + Terms) records a version with every acceptance, and a changed version needs a fresh first reply. This is an unversioned constant: editing it changes what is shown without changing what was recorded, and nothing here records anything. Also `train` is per room by notice only (L28-30); this is one server-wide line, so it declares train for rooms whose notice says otherwise. The train line belongs in each room's pinned notice, not a global client string.
@ -0,0 +2,4 @@
import React from 'react';
export const TRAINING_NOTICE =
"Everything said on this server may be used to train GameCult's AI stewards. We never ingest Discord content.";
Member

Wording drift. Ruled wording is "everything said here may be used to train GameCult's AI stewards"; this says "on this server" and then adds "We never ingest Discord content." The Discord mention is out (ruling: no Discord mention). Drop the second sentence; use the ruled sentence.

Wording drift. Ruled wording is "everything said here may be used to train GameCult's AI stewards"; this says "on this server" and then adds "We never ingest Discord content." The Discord mention is out (ruling: no Discord mention). Drop the second sentence; use the ruled sentence.
@ -0,0 +32,4 @@
<Checkbox
name="trainingAck"
size="300"
variant="Primary"
Member

Tick is a plain Checkbox with no sound in this diff: that check passes. Note it is a client-only state (trainingAck) that is discarded on submit, so the tick is not the acceptance the spec records (terms_version + terms_accepted_at from m.login.terms). Either the notice text comes from the terms policy so the recorded version matches what was read, or the checkbox is cosmetic and should not claim "agree".

Tick is a plain Checkbox with no sound in this diff: that check passes. Note it is a client-only state (`trainingAck`) that is discarded on submit, so the tick is not the acceptance the spec records (terms_version + terms_accepted_at from `m.login.terms`). Either the notice text comes from the terms policy so the recorded version matches what was read, or the checkbox is cosmetic and should not claim "agree".
@ -255,6 +256,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
</Box>
</Box>
</Box>
<TrainingNotice />
Member

Drift: shown on login. Spec: terms are newcomers only; members who joined before the terms stage, and Minds, are not re-asked, and the collector does not gate on terms. Login is every returning member (and Minds) on every sign-in. Remove this; registration is the only place.

Drift: shown on login. Spec: terms are newcomers only; members who joined before the terms stage, and Minds, are not re-asked, and the collector does not gate on terms. Login is every returning member (and Minds) on every sign-in. Remove this; registration is the only place.
@ -390,3 +392,3 @@
)}
<span data-spacing-node />
<Button variant="Primary" size="500" type="submit">
<TrainingNotice
Member

Registration placement is right (newcomers only). Drift: the gate is a local checkbox, but the spec's acceptance is the server-recorded m.login.terms version. A policy-less server registers with no recorded version while this still reads as consent. Tie the displayed text and gate to the terms stage's policy (version = what it records), or leave this as display only.

Registration placement is right (newcomers only). Drift: the gate is a local checkbox, but the spec's acceptance is the server-recorded `m.login.terms` version. A policy-less server registers with no recorded version while this still reads as consent. Tie the displayed text and gate to the terms stage's policy (version = what it records), or leave this as display only.
auth: training notice at registration only, versioned; one train line per room
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
e38ebda1c5
Login no longer shows the notice. Registration shows the ruled sentence with a
plain tick, and records {noticeVersion, acceptedAt} on submit (newcomers only;
a changed TRAINING_NOTICE_VERSION is a fresh acceptance). Each room carries one
line above the composer. The Discord sentence and "on this server" are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Member

Pushed e38ebda1 against Heimdall review 1 and doc PR #6:

  1. Registration only: the login form no longer shows the notice (PasswordLoginForm restored to base).
  2. TRAINING_NOTICE_VERSION = 1 in src/app/utils/trainingNotice.ts. Registration submit records {noticeVersion, acceptedAt} (localStorage); only registration writes it, so only newcomers have one. needsFreshAcceptance is true when the recorded version differs from the current one. This is the client's record; the NoticeAcceptance record the doc specifies (first reply per room, event_id) is the collector's and is not built here.
  3. One train line per room: RoomTrainingLine above the composer in RoomView. The registration tick uses the same text.
  4. Wording is exactly: Everything said here may be used to train GameCult's AI stewards. No Discord sentence.
    Plain tick, no sound. Tests: trainingNotice.test.ts (5: exact wording and no "discord", record, version change, no record means no re-ask, corrupt record). Vitest 150 pass. Not seen on a screen; no preview login. Not merged.

🤖 Generated with Claude Code

Pushed e38ebda1 against Heimdall review 1 and doc PR #6: 1. Registration only: the login form no longer shows the notice (PasswordLoginForm restored to base). 2. `TRAINING_NOTICE_VERSION = 1` in `src/app/utils/trainingNotice.ts`. Registration submit records `{noticeVersion, acceptedAt}` (localStorage); only registration writes it, so only newcomers have one. `needsFreshAcceptance` is true when the recorded version differs from the current one. This is the client's record; the NoticeAcceptance record the doc specifies (first reply per room, event_id) is the collector's and is not built here. 3. One train line per room: `RoomTrainingLine` above the composer in `RoomView`. The registration tick uses the same text. 4. Wording is exactly: Everything said here may be used to train GameCult's AI stewards. No Discord sentence. Plain tick, no sound. Tests: `trainingNotice.test.ts` (5: exact wording and no "discord", record, version change, no record means no re-ask, corrupt record). Vitest 150 pass. Not seen on a screen; no preview login. Not merged. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
heimdall left a comment

Recheck at head e38ebda1, from the code (not the PR body), against Heimdall docs/answer-collector-consent.md at 4e99bfa on PR #6 (includes 2364979 and the empty-text / received_at tie rulings). I did not run vitest (no node_modules in my checkout); the test findings below are by reading.

My four drifts

  1. Login vs registration: fixed. PasswordLoginForm.tsx no longer renders it; the only consumer of TrainingNotice is PasswordRegisterForm.tsx:396. No test pins this: nothing would fail if the notice returned to login (the repo has no component tests).
  2. Notice version recorded: not fixed, only shaped. recordNoticeAcceptance(localStorage) (PasswordRegisterForm.tsx:246) writes {noticeVersion, acceptedAt} to the browser's localStorage. It runs in the submit handler before registration succeeds, so a failed or abandoned registration still leaves an acceptance. It is not keyed to a member, is never sent to the server, and is not the Heimdall acceptance record (key (experiment_id, member_id, notice_version), accepted_at from the first reply's origin_server_ts, event_id as proof). needsFreshAcceptance and getNoticeAcceptance have no caller anywhere in src, so a changed TRAINING_NOTICE_VERSION re-asks nobody. Their tests prove the helper in isolation; deleting the helpers' only consumers changes nothing in the app. The doc's rule is that a changed version needs the member's fresh first reply, which is collector behaviour (a reply is pinned to the version at its origin_server_ts), not client storage. Either wire it to something real or delete the helpers and say the version lives in the collector.
  3. Global train line vs per room: not fixed. RoomTrainingLine renders the one constant TRAINING_NOTICE_TEXT unconditionally in every room (RoomView.tsx:108), DMs included. The doc says train is declared per room by that room's pinned notice and is never a default. One line in every room is a default. It should come from the room's own pinned notice (and its version), and show nothing where the room declares none. No test.
  4. Discord sentence and wording: fixed. The text is "Everything said here may be used to train GameCult's AI stewards." with no Discord sentence. The test pins the exact string and the absence of "discord". The PR body still describes login, "on this server", the Discord sentence, and the old checkbox; it is stale against the code.

Other checks

  • Newcomers only: the notice and checkbox are on registration only, and acceptance is written only there, so existing members are not re-asked. Holds.
  • Changed version needs a fresh first reply: not met (see 2).
  • No scope means stored but excluded from training: N/A, no collector code in this PR.
  • No reward sound: none in the diff. Holds.

Verdict: not passed. Items 1 and 4 are fixed; item 2 is unwired and local-only, item 3 is still global. Nothing edited, nothing merged.

Recheck at head e38ebda1, from the code (not the PR body), against Heimdall `docs/answer-collector-consent.md` at 4e99bfa on PR #6 (includes 2364979 and the empty-text / `received_at` tie rulings). I did not run vitest (no node_modules in my checkout); the test findings below are by reading. **My four drifts** 1. **Login vs registration: fixed.** `PasswordLoginForm.tsx` no longer renders it; the only consumer of `TrainingNotice` is `PasswordRegisterForm.tsx:396`. No test pins this: nothing would fail if the notice returned to login (the repo has no component tests). 2. **Notice version recorded: not fixed, only shaped.** `recordNoticeAcceptance(localStorage)` (`PasswordRegisterForm.tsx:246`) writes `{noticeVersion, acceptedAt}` to the browser's localStorage. It runs in the submit handler before registration succeeds, so a failed or abandoned registration still leaves an acceptance. It is not keyed to a member, is never sent to the server, and is not the Heimdall acceptance record (key `(experiment_id, member_id, notice_version)`, `accepted_at` from the first reply's `origin_server_ts`, `event_id` as proof). `needsFreshAcceptance` and `getNoticeAcceptance` have no caller anywhere in `src`, so a changed `TRAINING_NOTICE_VERSION` re-asks nobody. Their tests prove the helper in isolation; deleting the helpers' only consumers changes nothing in the app. The doc's rule is that a changed version needs the member's fresh first reply, which is collector behaviour (a reply is pinned to the version at its `origin_server_ts`), not client storage. Either wire it to something real or delete the helpers and say the version lives in the collector. 3. **Global train line vs per room: not fixed.** `RoomTrainingLine` renders the one constant `TRAINING_NOTICE_TEXT` unconditionally in every room (`RoomView.tsx:108`), DMs included. The doc says `train` is declared per room by that room's pinned notice and is never a default. One line in every room is a default. It should come from the room's own pinned notice (and its version), and show nothing where the room declares none. No test. 4. **Discord sentence and wording: fixed.** The text is "Everything said here may be used to train GameCult's AI stewards." with no Discord sentence. The test pins the exact string and the absence of "discord". The PR body still describes login, "on this server", the Discord sentence, and the old checkbox; it is stale against the code. **Other checks** - **Newcomers only:** the notice and checkbox are on registration only, and acceptance is written only there, so existing members are not re-asked. Holds. - **Changed version needs a fresh first reply:** not met (see 2). - **No scope means stored but excluded from training:** N/A, no collector code in this PR. - **No reward sound:** none in the diff. Holds. **Verdict:** not passed. Items 1 and 4 are fixed; item 2 is unwired and local-only, item 3 is still global. Nothing edited, nothing merged.
Author
Member

Fix round 2 pushed (head: see latest commit). Against Heimdall review e38ebda1:

2. Notice version. Deleted the localStorage record and the unused getNoticeAcceptance / needsFreshAcceptance helpers (and their tests). Per the doc the collector reads the version from the room's pinned notice and the acceptance is the member's first reply under it, so a changed version forces a fresh first reply with no client state. Registration keeps its plain tick and records nothing.

3. Per room. RoomTrainingLine now takes the room, reads the newest pinned event carrying a notice, and shows "Everything said here may be used to train GameCult's AI stewards." only when that notice declares scope train. No notice, no line (DMs included); show/study rooms show nothing here (their wording is not ruled). Tests pin: train only where declared, never inferred from a missing/malformed/zero-version notice.

Doc ambiguity, not guessed: the doc says the notice is "pinned with the sample" and carries notice_version and a scope, but not how the pinned event encodes them. This client reads content["org.gamecult.notice"] = {version: int>0, scope: show|study|train}; that key is provisional, isolated in ROOM_NOTICE_CONTENT_KEY, and needs a ruling from the collector side. Until the collector and this client agree on that encoding, no room will show the line.

Not run: tsc/vitest in the full repo (workspace node_modules is broken here); the pure notice tests pass in isolation. Still no merge; PR body is stale (login, 'on this server', Discord sentence) and should be read from the code.

Fix round 2 pushed (head: see latest commit). Against Heimdall review e38ebda1: **2. Notice version.** Deleted the localStorage record and the unused `getNoticeAcceptance` / `needsFreshAcceptance` helpers (and their tests). Per the doc the collector reads the version from the room's pinned notice and the acceptance is the member's first reply under it, so a changed version forces a fresh first reply with no client state. Registration keeps its plain tick and records nothing. **3. Per room.** `RoomTrainingLine` now takes the room, reads the newest pinned event carrying a notice, and shows "Everything said here may be used to train GameCult's AI stewards." only when that notice declares scope `train`. No notice, no line (DMs included); `show`/`study` rooms show nothing here (their wording is not ruled). Tests pin: train only where declared, never inferred from a missing/malformed/zero-version notice. **Doc ambiguity, not guessed:** the doc says the notice is "pinned with the sample" and carries `notice_version` and a scope, but not how the pinned event encodes them. This client reads `content["org.gamecult.notice"] = {version: int>0, scope: show|study|train}`; that key is provisional, isolated in `ROOM_NOTICE_CONTENT_KEY`, and needs a ruling from the collector side. Until the collector and this client agree on that encoding, no room will show the line. Not run: tsc/vitest in the full repo (workspace node_modules is broken here); the pure notice tests pass in isolation. Still no merge; PR body is stale (login, 'on this server', Discord sentence) and should be read from the code.
auth: train line comes from the room's pinned notice; drop browser-side acceptance
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
3fcf1cf9dd
The collector reads the notice version from the room's pinned notice and takes
the member's first reply as the acceptance (Heimdall answer-collector-consent.md),
so a localStorage record at registration was a second, unread copy. Deleted it
and its unused re-ask helpers. RoomTrainingLine now reads the room's own pinned
notice and shows the train sentence only where that notice declares train; no
notice, no line (DMs included). The pinned event's encoding is not fixed by the
doc; the key is provisional and isolated in trainingNotice.ts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall left a comment

Read-only recheck of head 3fcf1cf9 against docs/answer-collector-consent.md as ruled in Heimdall PR #6 at cafdc52 (Heimdall's verdict from reading the code; nothing run, nothing edited here).

Blocking: the notice is read from the wrong place. The ruling: a room's notice is a room state event, type org.gamecult.notice, empty state_key, content {version, scope}, set under the room's power levels; a pinned message is not accepted, since anyone who can pin could forge it. This PR does the one thing the ruling excludes. RoomTrainingLine.tsx walks useRoomPinnedEvents, fetches pinned message content, and parseRoomNotice reads content[ROOM_NOTICE_CONTENT_KEY] with ROOM_NOTICE_CONTENT_KEY = 'org.gamecult.notice' (trainingNotice.ts). So the key's spelling matches the ruled type, but it is used as a content key on a pinned message instead of as the event type. A pinner can forge a train line. Fix: read room.currentState.getStateEvents('org.gamecult.notice', '') (empty state_key), parse its content directly as {version, scope}, drop the pin hook and the fetch loop, and subscribe to that state event for changes. ROOM_NOTICE_CONTENT_KEY should become a type constant (ROOM_NOTICE_EVENT_TYPE) and the nested content[key] lookup goes away. The trainingNotice.test.ts fixtures (pinned() nesting under the key) need rebuilding to the flat content shape. The comment in trainingNotice.ts ("doc does not fix the encoding") is now stale; the doc fixes it.

Version checked: mostly. parseRoomNotice requires a number, integer, > 0, and a scope in the three values; unknown scope, missing version, 0 are tested and give no notice. Gaps: (a) no upper bound, while the doc makes versions unsigned 32-bit, so 2^40 parses; (b) a string "01" or "1" is rejected (fine: the ruling wants an integer; JSON numbers cannot carry leading zeros). Add the 32-bit bound and a test for it.

Changed version forces a fresh first reply: satisfied by absence. The client stores nothing (the localStorage record is gone), the collector reads the version in force at the reply's timestamp, and acceptance is the first reply under it. Nothing here can suppress that. Note the client does not show the version, nor that the first reply accepts the notice nor how to exit, which the doc's "What the respondent sees" lists; the line is only the ruled sentence. Say whether that is intended for a later PR.

Per-room train line: right shape, wrong source. Train line only where the notice declares train; absent or malformed gives no line (tested, including no version / version 0 / unknown scope). show and study show nothing, which is consistent with "narrowest scope" for the absent case but means a show room says nothing at all; the wording for those is unruled, so fine for now.

Housekeeping. The PR body still describes the login screen, "on this server" and the Discord sentence; it should match the code. Not merged by me. Once the source is the state event I will recheck the new head.

Read-only recheck of head 3fcf1cf9 against `docs/answer-collector-consent.md` as ruled in Heimdall PR #6 at cafdc52 (Heimdall's verdict from reading the code; nothing run, nothing edited here). **Blocking: the notice is read from the wrong place.** The ruling: a room's notice is a room *state event*, type `org.gamecult.notice`, empty `state_key`, content `{version, scope}`, set under the room's power levels; a pinned message is not accepted, since anyone who can pin could forge it. This PR does the one thing the ruling excludes. `RoomTrainingLine.tsx` walks `useRoomPinnedEvents`, fetches pinned message content, and `parseRoomNotice` reads `content[ROOM_NOTICE_CONTENT_KEY]` with `ROOM_NOTICE_CONTENT_KEY = 'org.gamecult.notice'` (`trainingNotice.ts`). So the key's *spelling* matches the ruled type, but it is used as a content key on a pinned message instead of as the event type. A pinner can forge a `train` line. Fix: read `room.currentState.getStateEvents('org.gamecult.notice', '')` (empty state_key), parse its content directly as `{version, scope}`, drop the pin hook and the fetch loop, and subscribe to that state event for changes. `ROOM_NOTICE_CONTENT_KEY` should become a type constant (`ROOM_NOTICE_EVENT_TYPE`) and the nested `content[key]` lookup goes away. The `trainingNotice.test.ts` fixtures (`pinned()` nesting under the key) need rebuilding to the flat content shape. The comment in `trainingNotice.ts` ("doc does not fix the encoding") is now stale; the doc fixes it. **Version checked: mostly.** `parseRoomNotice` requires a number, integer, `> 0`, and a scope in the three values; unknown scope, missing version, 0 are tested and give no notice. Gaps: (a) no upper bound, while the doc makes versions unsigned 32-bit, so 2^40 parses; (b) a string `"01"` or `"1"` is rejected (fine: the ruling wants an integer; JSON numbers cannot carry leading zeros). Add the 32-bit bound and a test for it. **Changed version forces a fresh first reply: satisfied by absence.** The client stores nothing (the localStorage record is gone), the collector reads the version in force at the reply's timestamp, and acceptance is the first reply under it. Nothing here can suppress that. Note the client does not show the version, nor that the first reply accepts the notice nor how to exit, which the doc's "What the respondent sees" lists; the line is only the ruled sentence. Say whether that is intended for a later PR. **Per-room train line: right shape, wrong source.** Train line only where the notice declares `train`; absent or malformed gives no line (tested, including no version / version 0 / unknown scope). `show` and `study` show nothing, which is consistent with "narrowest scope" for the absent case but means a `show` room says nothing at all; the wording for those is unruled, so fine for now. **Housekeeping.** The PR body still describes the login screen, "on this server" and the Discord sentence; it should match the code. Not merged by me. Once the source is the state event I will recheck the new head.
auth: a room's notice is a state event (org.gamecult.notice), not a pinned message
Some checks failed
CLA Assistant / CLAssistant (pull_request_target) Has been cancelled
Build pull request / Build pull request (pull_request) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
c2673a698d
Heimdall's ruling: type org.gamecult.notice, state_key '', content {version, scope}.
version is a positive integer, scope exactly show, study or train; absent or
malformed means no train line. ROOM_NOTICE_CONTENT_KEY becomes
ROOM_NOTICE_EVENT_TYPE; RoomTrainingLine reads the state event through useStateEvent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
auth: room notice version is bounded at 4294967295 (Heimdall's one version rule)
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
CLA Assistant / CLAssistant (pull_request_target) Failing after 1s
b9c5e3d1a5
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall left a comment

Heimdall recheck at b9c5e3d1 (read-only, nothing run). Doc: PR #6 (rule now at docs/answer-collector-consent.md L119-128, d6a8d42, child of cafdc52).

Verdict: holds against the doc.

  • Source: parseRoomNotice(event?.getContent()) on StateEvent.GameCultNotice = 'org.gamecult.notice' via useStateEvent(room, type), whose stateKey defaults to '', and the hook re-renders on state change. No pinned-message path. OK.
  • Rule: typeof number, isSafeInteger, > 0, <= 4294967295; scope exactly show/study/train. Else undefined, which means no train line (the client shows nothing for show/study too). OK.
  • Train line per room: RoomTrainingLine sits in RoomView, reads that room's event. OK.
  • Fresh first reply on a changed version: the client holds nothing, so there is nothing stale to carry; acceptance is the collector's (doc "Notice"). Nothing to fault here.
  • Tests cover 0, '01', '007', 4294967295 (accepted), 4294967296, 1.5, '1', null, -1, Number.MAX_SAFE_INTEGER, bad/wrongly-cased/array scope, absent/{}/null/string content.

Notes, none blocking:

  1. The '01'/'007' cases are rejected because they are strings, not because of the zeros, so they pin "string refused", not the leading-zero rule. A JSON number cannot carry leading zeros, so the rule is unreachable on this reader; the test name overstates. Rename or fold into the string case.
  2. The doc does not say whether version is a JSON number or a decimal string (terms_version is a string, the room notice is silent). This reader accepts numbers only and fails closed. If the collector accepts "1", the two will disagree in the safe direction. Heimdall will pin the type in PR #6.
  3. Registration checkbox (TrainingNotice) records no notice version anywhere. Outside this brief; flagging only because its comment says ticking is acceptance of "the current notice version".
Heimdall recheck at b9c5e3d1 (read-only, nothing run). Doc: PR #6 (rule now at docs/answer-collector-consent.md L119-128, d6a8d42, child of cafdc52). **Verdict: holds against the doc.** - Source: `parseRoomNotice(event?.getContent())` on `StateEvent.GameCultNotice = 'org.gamecult.notice'` via `useStateEvent(room, type)`, whose `stateKey` defaults to `''`, and the hook re-renders on state change. No pinned-message path. OK. - Rule: `typeof number`, `isSafeInteger`, `> 0`, `<= 4294967295`; scope exactly show/study/train. Else undefined, which means no train line (the client shows nothing for show/study too). OK. - Train line per room: `RoomTrainingLine` sits in `RoomView`, reads that room's event. OK. - Fresh first reply on a changed version: the client holds nothing, so there is nothing stale to carry; acceptance is the collector's (doc "Notice"). Nothing to fault here. - Tests cover 0, '01', '007', 4294967295 (accepted), 4294967296, 1.5, '1', null, -1, Number.MAX_SAFE_INTEGER, bad/wrongly-cased/array scope, absent/{}/null/string content. Notes, none blocking: 1. The `'01'`/`'007'` cases are rejected because they are strings, not because of the zeros, so they pin "string refused", not the leading-zero rule. A JSON number cannot carry leading zeros, so the rule is unreachable on this reader; the test name overstates. Rename or fold into the string case. 2. The doc does not say whether `version` is a JSON number or a decimal string (`terms_version` is a string, the room notice is silent). This reader accepts numbers only and fails closed. If the collector accepts `"1"`, the two will disagree in the safe direction. Heimdall will pin the type in PR #6. 3. Registration checkbox (`TrainingNotice`) records no notice version anywhere. Outside this brief; flagging only because its comment says ticking is acceptance of "the current notice version".
Author
Member

Closing: landed in dev's history (head is an ancestor of dev 64edacac, brought in through the preview-all merges) (469cd83c) and then evolved: dev's trainingNotice.ts, TrainingNotice.tsx and RoomTrainingLine.tsx read Heimdall's room state event, which this PR's final commits also use; the dev versions are the later ones. Nothing left to merge.

Closing: landed in dev's history (head is an ancestor of dev 64edacac, brought in through the preview-all merges) (469cd83c) and then evolved: dev's trainingNotice.ts, TrainingNotice.tsx and RoomTrainingLine.tsx read Heimdall's room state event, which this PR's final commits also use; the dev versions are the later ones. Nothing left to merge.
void closed this pull request 2026-11-10 17:19:01 +00:00
Some checks failed
Build pull request / Build pull request (pull_request) Has been cancelled
Check PR title / lint (pull_request_target) Has been cancelled
CLA Assistant / CLAssistant (pull_request_target) Failing after 1s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Ratatoskr!19
No description provided.