Align answer-collector consent with #swarm rulings of 2026-11-06
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>
This commit is contained in:
parent
e8edd88efc
commit
d836f29e3a
1 changed files with 106 additions and 52 deletions
|
|
@ -2,49 +2,103 @@
|
|||
|
||||
Status: specification. Nothing here is implemented. It fixes what an answer
|
||||
record is and what a respondent has agreed to, so that a collector, a tally and
|
||||
a training export can be built against one definition.
|
||||
a training export can be built against one definition. Field names and types
|
||||
below are the contract for the typed CultCache records.
|
||||
|
||||
Scope: free-text and choice answers that members give to a sample put to them
|
||||
by an experiment. Not identity proof (see `architecture.md`).
|
||||
by an experiment. Not identity proof (see `architecture.md`). Personal details
|
||||
(names, contact data, account identifiers, anything that identifies a person) are
|
||||
out of scope: the collector does not ask for them and a sample must not solicit
|
||||
them. The only identifier it keeps is `member_id`, and that stays on the reward
|
||||
path.
|
||||
|
||||
## Record
|
||||
## Notice
|
||||
|
||||
Consent is declared once per room, not per reply. Each experiment room carries
|
||||
one notice line, pinned with the sample, that names the scope the room's answers
|
||||
may be used for and the way out. A notice has a `notice_version`; any change to
|
||||
its wording or scope is a new version.
|
||||
|
||||
| scope | what the notice allows |
|
||||
|---|---|
|
||||
| `show` | display back inside the experiment (to the respondent, and in aggregate to participants). Nothing else. |
|
||||
| `study` | `show`, plus analysis by the experiment's researchers. Quoted in findings only unattributed. |
|
||||
| `train` | `study`, plus use as training or evaluation data for models. |
|
||||
|
||||
`train` is a declared scope: it is used only when the notice for that room says
|
||||
`train` in terms. It is never inferred, never a default, and never raised by a
|
||||
reply.
|
||||
|
||||
The first reply a member posts in the room is their acceptance of the notice
|
||||
version then pinned. No separate click, command or form. A later notice version
|
||||
needs a new acceptance: the member's first reply under it.
|
||||
|
||||
### Notice acceptance record
|
||||
|
||||
| field | type | rule |
|
||||
|---|---|---|
|
||||
| `member_id` | Heimdall member id | the accepting member. |
|
||||
| `experiment_id` | string | the room. |
|
||||
| `notice_version` | integer | the version pinned when the reply was received. |
|
||||
| `scope` | `show` \| `study` \| `train` | the scope that version declared. |
|
||||
| `accepted_at` | UTC timestamp | `origin_server_ts` of the first reply. |
|
||||
| `event_id` | Matrix event id | that reply, as proof. |
|
||||
|
||||
Key: `(experiment_id, member_id, notice_version)`. Written once, never updated.
|
||||
|
||||
## Terms (newcomers only)
|
||||
|
||||
Joining Heimdall is gated by terms. Acceptance is the terms version recorded by
|
||||
the Matrix `m.login.terms` stage at registration: member field `terms_version`
|
||||
(string) and `terms_accepted_at` (UTC timestamp). This applies to newcomers
|
||||
only. Members who joined before the terms stage existed, and Minds, are not
|
||||
re-asked and have no `terms_version`; the collector does not read it and does not
|
||||
gate on it. The notice above is the only consent the collector itself checks.
|
||||
|
||||
## Answer record
|
||||
|
||||
One record per answer. Fields:
|
||||
|
||||
| field | rule |
|
||||
|---|---|
|
||||
| `sample_id` | names the thing answered about, with a version component (`<sample>@<version>`). A changed sample is a new version. |
|
||||
| `experiment_id` | the experiment that asked. |
|
||||
| `member_id` | Heimdall member id of the respondent. Never a Matrix id, never a display name. |
|
||||
| `scope` | `show` \| `study` \| `train`. Default `show`. |
|
||||
| `text` | the answer as given. |
|
||||
| `revoked` | boolean, default false. One-way: a revoked record is never un-revoked; a new answer is a new record. |
|
||||
| `received_at` | collector clock, UTC. |
|
||||
| field | type | rule |
|
||||
|---|---|---|
|
||||
| `sample_id` | string `<sample>@<version>` | names the thing answered about; `<version>` is a positive integer. A changed sample is a new version. |
|
||||
| `experiment_id` | string | the experiment that asked. |
|
||||
| `member_id` | Heimdall member id | the respondent. Never a Matrix id, never a display name. |
|
||||
| `notice_version` | integer | the notice version in force when the answer was received; matches an acceptance record by the key above. |
|
||||
| `scope` | `show` \| `study` \| `train` | copied from that notice version at answer time. Never edited afterwards. |
|
||||
| `text` | string, or absent | the answer as given. Absent once exited. |
|
||||
| `exited` | boolean, default false | one-way: set by redaction, never cleared. A new answer is a new record. |
|
||||
| `received_at` | UTC timestamp | `origin_server_ts` of the reply. |
|
||||
| `event_id` | Matrix event id | the reply; the receipt. |
|
||||
|
||||
Key: `(experiment_id, sample_id, member_id)`. A member's later answer to the
|
||||
same key replaces the earlier one (latest `received_at` wins); the earlier text
|
||||
is not retained.
|
||||
|
||||
A record whose `notice_version` or `scope` is missing, unparseable or has no
|
||||
matching acceptance record is not collected. It is excluded from every use, and
|
||||
from training in particular.
|
||||
|
||||
## Versions
|
||||
|
||||
Answers never tally across `sample_id` versions. A tally is computed for one
|
||||
full `sample_id` including version; there is no "all versions" aggregate and no
|
||||
API that takes a sample without a version. A version bump starts an empty tally.
|
||||
Replies to an old version are dropped when the tally is for a newer one.
|
||||
|
||||
## Scope
|
||||
## Training rule
|
||||
|
||||
Scope is the widest use the respondent allowed. Each use reads exactly one
|
||||
level:
|
||||
A record is training or evaluation data only if all hold:
|
||||
|
||||
- `show`: may be displayed back inside the experiment (to the respondent, and
|
||||
in aggregate to participants). Nothing else.
|
||||
- `study`: `show`, plus analysis by the experiment's researchers. May be quoted
|
||||
in findings only unattributed.
|
||||
- `train`: `study`, plus use as training or evaluation data for models.
|
||||
1. `scope = train`, copied from a notice version that declared `train`;
|
||||
2. an acceptance record exists for `(experiment_id, member_id, notice_version)`
|
||||
with `accepted_at <= received_at`, i.e. the author had accepted that notice
|
||||
version when they answered;
|
||||
3. `exited = false` at export time.
|
||||
|
||||
Rule: nothing is training data unless `scope = train`. A missing, unparseable
|
||||
or unknown scope is `show`, never wider. Scope can be narrowed by a new answer
|
||||
or revocation; widening is a new answer by the respondent, never an inference.
|
||||
No recorded scope, or no recorded acceptance, means excluded. Records wider than
|
||||
`show` are kept apart from `show` records: separate store or partition, so a read
|
||||
for one level cannot return another.
|
||||
|
||||
## Identity: count once, reward without exposure
|
||||
|
||||
|
|
@ -63,56 +117,56 @@ not stored under a placeholder.
|
|||
## Eureka's shape (Matrix)
|
||||
|
||||
- One room per experiment. The room is `experiment_id`.
|
||||
- The sample is posted as a message; an answer is a thread reply to it whose
|
||||
body starts `scope: <x>` on its first line, the answer text following.
|
||||
- The sample is posted as a message, with the notice pinned beside it. An answer
|
||||
is a thread reply to the sample, the answer text being the body. There is no
|
||||
per-reply scope syntax.
|
||||
- The reply event is the receipt: its event id and timestamp are the proof the
|
||||
answer was received, and `received_at` comes from it.
|
||||
- Redaction is revoke: redacting the reply event sets `revoked`.
|
||||
- Redaction is exit (see below): redacting the reply event sets `exited`.
|
||||
- The collector is on demand (it reads the room when run, it is not a standing
|
||||
listener). It drops:
|
||||
- redacted replies;
|
||||
- replies with no `scope:` line (consent is explicit in Matrix; the `show`
|
||||
default applies to records created elsewhere, not to silence);
|
||||
- redacted replies (recorded as exited);
|
||||
- replies from unlinked senders;
|
||||
- replies to an old sample version when the tally is for a newer one.
|
||||
- Records wider than `show` (`study`, `train`) are kept apart from `show`
|
||||
records: separate store or partition, so a read for one level cannot
|
||||
accidentally return another.
|
||||
|
||||
## What Matrix cannot guarantee
|
||||
|
||||
- **Receipt outliving a deleted reply.** Redaction removes content, and
|
||||
homeserver retention varies. If the collector holds a receipt for a reply that
|
||||
later disappears, it keeps a tombstone: event id, `sample_id`, `received_at`,
|
||||
and a hash of `text` (hash only, no text), so the count and the revoke are
|
||||
provable without the content. The record is marked `revoked`; `text` is
|
||||
erased.
|
||||
later disappears, it keeps a tombstone: `event_id`, `sample_id`, `received_at`,
|
||||
and a salted hash of `text`. Hash only, never text, and no `member_id`
|
||||
beyond what the record's key already holds. The count and the exit are
|
||||
provable without the content. The record is marked `exited`; `text` is erased.
|
||||
- **Edits.** An edit is a new event, not the receipt. Only the original event
|
||||
counts; edits are ignored, so what was consented to is what was received. To
|
||||
counts; edits are ignored, so what was accepted is what was received. To
|
||||
change an answer, redact and reply again.
|
||||
- **Bridge spoofing.** A bridge can post as any ghost user. Only senders whose
|
||||
Matrix id resolves through Heimdall's linked identity to a member count;
|
||||
bridged ghost ids are not linked identities. A bridge cannot raise scope, as
|
||||
scope comes only from the reply's own `scope:` line.
|
||||
- **Discord material.** Nothing that originates from Discord is collected,
|
||||
even through a bridge, at any scope: Discord forbids training on its
|
||||
content. Bridged events are dropped by origin, not by judging text.
|
||||
bridged ghost ids are not linked identities. A bridge cannot raise scope: scope
|
||||
comes only from the pinned notice version.
|
||||
- **Bridged origin.** Events that arrive through a bridge are dropped by origin,
|
||||
not by judging their text.
|
||||
|
||||
## What the respondent sees
|
||||
|
||||
Posted with the sample, in the room:
|
||||
|
||||
- the three scopes in one line each, as above;
|
||||
- that no `scope:` line means the answer is not collected;
|
||||
- how to revoke.
|
||||
- the notice: its scope in one line, that the first reply accepts it, and how to
|
||||
exit;
|
||||
- the notice version.
|
||||
|
||||
After answering, the respondent sees their receipt: the thread reply itself,
|
||||
plus the collector's acknowledgement that names the scope it recorded. If it
|
||||
recorded narrower than they asked, it says so.
|
||||
plus the collector's acknowledgement that names the notice version and scope it
|
||||
recorded.
|
||||
|
||||
## How to revoke
|
||||
The consent and vote controls make no reward sound, animation or other
|
||||
reinforcement. A reward is issued later, by the rewarding service, never as
|
||||
feedback to the act of consenting or voting.
|
||||
|
||||
Redact your reply. That is all. The next collection run marks the record
|
||||
`revoked`, erases `text`, and excludes it from every tally and export. Anything
|
||||
already exported at `train` before the run cannot be recalled by Matrix; the
|
||||
experiment's page states the export cadence so the window is known.
|
||||
## How to exit
|
||||
|
||||
Redact your reply. That is all. The next collection run sets `exited`, erases
|
||||
`text`, and excludes the record from every later tally and from training. It
|
||||
cannot undo past use: tallies already shown and anything already exported
|
||||
cannot be recalled by Matrix or by the collector. The experiment's page states
|
||||
the export cadence so the window is known.
|
||||
|
|
|
|||
Loading…
Reference in a new issue