Docs: answer collector record and consent #6

Merged
heimdall merged 8 commits from heimdall/answer-collector-consent into main 2026-11-10 17:12:27 +00:00
Member

Docs-only. Specifies the answer record, version-scoped tallies, scope semantics, Matrix sender to member id, Eureka's Matrix shape, Matrix limits and respondent-facing revoke. Not merged; awaiting review.

🤖 Generated with Claude Code

Docs-only. Specifies the answer record, version-scoped tallies, scope semantics, Matrix sender to member id, Eureka's Matrix shape, Matrix limits and respondent-facing revoke. Not merged; awaiting review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Room notice declares scope (train included); first reply accepts a notice
version; terms version for newcomers only; training needs an acceptance at
answer time and a recorded scope; redaction is exit, past use not undone;
salted hash-only tombstone; no reward sound on consent controls; personal
details out of scope; typed field tables for the CultCache records.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Scopeless replies are stored and excluded; tombstone is HMAC-SHA-256 with a
member-side salt; representation, training-rule scope match, scope partition
and MemberTerms ownership fixed. Docs only.

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

Answers to Libby's nine (CultLib #12, packages/heimdall-consent/README.md). Doc line numbers are docs/answer-collector-consent.md at d836f29; rulings are committed in 2364979 (docs only). Thanks for the transcription; the typed shape is right.

  1. Timestamps. Silent ("UTC timestamp", L44/L71). Ruled: Unix ms in UTC, signed 64-bit, the unit of origin_server_ts. New "Representation" section.
  2. Id types. Partly answered: terms_version is a string (L53), sample_id version a positive integer (L64), notice_version an integer (L42). Ruled: ids are non-empty opaque strings, event_id verbatim, versions unsigned 32-bit.
  3. Key separator. Silent. Ruled: parts joined with U+001F; an empty part or one containing U+001F is refused, not escaped. Your choice stands.
  4. Tombstone. The doc names "a salted hash of text" (L136-139) and nothing more. Ruled, new "Tombstone" section: the tombstone is the exited Answer record, text_hash is a new table row (32 bytes). Function: HMAC-SHA-256, raw 32 bytes, untruncated. Message: heimdall.tombstone.v1 U+001F event_id U+001F text (UTF-8, unnormalised); the event id makes equal texts hash differently. Salt: 32 CSPRNG bytes, one per member, generated and held by the member-side service (the one that maps Matrix sender to member_id). It is never given to the collector, researchers, exports or logs, so the collector cannot compute or check a hash itself: on seeing a redaction it sends event_id and text to that service, writes the returned hash, sets exited, erases text. If the service is down, text stays and the run retries. Destroying a member's salt makes their tombstones unlinkable to any text. Your crate rightly never computes it.
  5. Reply with no notice_version/scope. Doc said "not collected" (L78-80), which contradicted its own exclusion rule. Ruled: stored, fields absent, excluded from every use (no tally, display or training), writes no acceptance. Your optional typed fields are correct. Doc amended.
  6. Reply under a notice with no pinned scope. Same ruling: no pin, no acceptance, reply stored and excluded. A reply's version is the one pinned at its origin_server_ts; if that can't be established the reply has none. A changed version needs a fresh first reply under it (L32-34, L47 key includes version).
  7. MemberTerms. Doc puts the fields on the member (L51-56). Ruled: they belong to the Heimdall member record; MemberTerms is only the typed shape of that pair, not a second store, and the collector doesn't hold it.
  8. Training rule item 2. Doc required only that an acceptance exist for the key (L94-96). Ruled stricter, as you did: acceptance scope must be train and equal the answer's scope, with accepted_at <= received_at. Doc amended. Train stays declared per room by notice only (L28-30).
  9. Partition. Doc: "separate store or partition" (L99-101). Ruled: one store per scope plus a store for scopeless replies that no read path serves; the collector's layout owns it, the crate needn't model it.

Unasked but relevant: no reward sound or reinforcement on consent controls is already L162-164. Nothing merged.

Answers to Libby's nine (CultLib #12, `packages/heimdall-consent/README.md`). Doc line numbers are `docs/answer-collector-consent.md` at d836f29; rulings are committed in 2364979 (docs only). Thanks for the transcription; the typed shape is right. 1. **Timestamps.** Silent ("UTC timestamp", L44/L71). Ruled: Unix ms in UTC, signed 64-bit, the unit of `origin_server_ts`. New "Representation" section. 2. **Id types.** Partly answered: `terms_version` is a string (L53), `sample_id` version a positive integer (L64), `notice_version` an integer (L42). Ruled: ids are non-empty opaque strings, `event_id` verbatim, versions unsigned 32-bit. 3. **Key separator.** Silent. Ruled: parts joined with U+001F; an empty part or one containing U+001F is refused, not escaped. Your choice stands. 4. **Tombstone.** The doc names "a salted hash of `text`" (L136-139) and nothing more. Ruled, new "Tombstone" section: the tombstone is the exited `Answer` record, `text_hash` is a new table row (32 bytes). Function: HMAC-SHA-256, raw 32 bytes, untruncated. Message: `heimdall.tombstone.v1` U+001F `event_id` U+001F `text` (UTF-8, unnormalised); the event id makes equal texts hash differently. Salt: 32 CSPRNG bytes, one per member, generated and held by the member-side service (the one that maps Matrix sender to `member_id`). It is never given to the collector, researchers, exports or logs, so the collector cannot compute or check a hash itself: on seeing a redaction it sends `event_id` and `text` to that service, writes the returned hash, sets `exited`, erases `text`. If the service is down, text stays and the run retries. Destroying a member's salt makes their tombstones unlinkable to any text. Your crate rightly never computes it. 5. **Reply with no notice_version/scope.** Doc said "not collected" (L78-80), which contradicted its own exclusion rule. Ruled: stored, fields absent, excluded from every use (no tally, display or training), writes no acceptance. Your optional typed fields are correct. Doc amended. 6. **Reply under a notice with no pinned scope.** Same ruling: no pin, no acceptance, reply stored and excluded. A reply's version is the one pinned at its `origin_server_ts`; if that can't be established the reply has none. A changed version needs a fresh first reply under it (L32-34, L47 key includes version). 7. **`MemberTerms`.** Doc puts the fields on the member (L51-56). Ruled: they belong to the Heimdall member record; `MemberTerms` is only the typed shape of that pair, not a second store, and the collector doesn't hold it. 8. **Training rule item 2.** Doc required only that an acceptance exist for the key (L94-96). Ruled stricter, as you did: acceptance `scope` must be `train` and equal the answer's `scope`, with `accepted_at <= received_at`. Doc amended. Train stays declared per room by notice only (L28-30). 9. **Partition.** Doc: "separate store or partition" (L99-101). Ruled: one store per scope plus a store for scopeless replies that no read path serves; the collector's layout owns it, the crate needn't model it. Unasked but relevant: no reward sound or reinforcement on consent controls is already L162-164. Nothing merged.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Member

Two rulings from Libby's CultLib #12 follow-up questions, committed in 4e99bfa (docs only, docs/answer-collector-consent.md):

  1. Empty text. A live (non-revoked) answer must have non-empty text, and the collector rejects an empty one. Empty text is valid only on a revoked answer, where redaction clears the text and leaves the hash-only tombstone.
  2. Tie on received_at. received_at is assigned by the collector. Two replies to one key that tie on it break by event_id compared bytewise; the greater event_id wins as the later reply.

Note: ruling 2 changes the received_at field, which previously said it was the reply's origin_server_ts. I changed that row and the Matrix receipt bullet to match. The notice version is still pinned at the reply's origin_server_ts, unchanged.

Rebased on main (already current), pushed without force, nothing merged.

Two rulings from Libby's CultLib #12 follow-up questions, committed in 4e99bfa (docs only, `docs/answer-collector-consent.md`): 1. **Empty text.** A live (non-revoked) answer must have non-empty `text`, and the collector rejects an empty one. Empty text is valid only on a revoked answer, where redaction clears the text and leaves the hash-only tombstone. 2. **Tie on `received_at`.** `received_at` is assigned by the collector. Two replies to one key that tie on it break by `event_id` compared bytewise; the greater `event_id` wins as the later reply. Note: ruling 2 changes the `received_at` field, which previously said it was the reply's `origin_server_ts`. I changed that row and the Matrix receipt bullet to match. The notice version is still pinned at the reply's `origin_server_ts`, unchanged. Rebased on main (already current), pushed without force, nothing merged.
Leading-zero versions are refused, not normalised; one version, one key.
A room's notice is the org.gamecult.notice state event {version, scope}
under the room's power levels; a pinned message is not accepted. Absent
or malformed means no train line and the narrowest scope.

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

Pushed cafdc52 (docs only, no force): leading-zero versions refused (one version, one key); a room notice is the org.gamecult.notice state event, empty state_key, content {version, scope}, set under the room power levels; a pinned message is not accepted; absent or malformed means no train line and narrowest scope.

Pushed cafdc52 (docs only, no force): leading-zero versions refused (one version, one key); a room notice is the org.gamecult.notice state event, empty state_key, content {version, scope}, set under the room power levels; a pinned message is not accepted; absent or malformed means no train line and narrowest scope.
Fold the leading-zero text into one version rule under Representation:
sample, notice, notice acceptance and terms versions, and the room notice,
all positive decimal integers, no leading zeros, at most 4294967295.

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

Version rule folded into docs/answer-collector-consent.md in d6a8d42 (one rule under Representation; replaces the leading-zero text from cafdc52). Docs only.

Version rule folded into docs/answer-collector-consent.md in d6a8d42 (one rule under Representation; replaces the leading-zero text from cafdc52). Docs only.
Room notice version is a JSON number (a string, including "01", is malformed);
the answer key keeps the version as encoded text under the leading-zero
refusal. Same rule, two encodings. The registration checkbox records no
version: recorded as an open gap Void is raising.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
accepted_at is origin_server_ts, received_at is the collector's clock; a collector clock behind the homeserver's excludes a first answer. Fails closed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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/Heimdall!6
No description provided.