heimdall-consent: typed records for the answer collector #12

Merged
libby merged 8 commits from libby/consent-records into main 2026-11-07 08:28:31 +00:00
Member

Typed CultCache records for the answer collector, cut from Heimdall's spec. Credit to Heimdall: this is their design, docs/answer-collector-consent.md in GameCult/Heimdall PR #6 (head aaeb175; one-version-rule at d6a8d42). I only transcribed it into records and a golden vector.

New crate packages/heimdall-consent (sibling of heimdall-stake): NoticeAcceptance, Answer, MemberTerms, plus Answer::is_training_data and Answer::redacted. Vector: tests/vectors/consent_golden.csv, run by tests/golden.rs against a real CultCache. cargo test: 14 passed.

Doc lines the vector pins:

  • Key includes the version: "Key: (experiment_id, sample_id, member_id)" with "sample_id | string <sample>@<version> ... A changed sample is a new version."
  • No scope, no training: "No recorded scope, or no recorded acceptance, means excluded." and "A record whose notice_version or scope is missing, unparseable or has no matching acceptance record is not collected."
  • Redaction is a hash-only revoke: "it keeps a tombstone: event_id, sample_id, received_at, and a salted hash of text. Hash only, never text ... The record is marked exited; text is erased."
  • Newcomer: "Acceptance is the terms version recorded by the Matrix m.login.terms stage at registration" and "The first reply a member posts in the room is their acceptance of the notice version then pinned." Notice key: (experiment_id, member_id, notice_version), "Written once, never updated."

Questions for Heimdall (full list in the crate README): timestamp representation (unix ms here); id types; key separator; where the tombstone's salted hash lives, since the answer table has no row for it (carried as Answer.text_hash, hash function and salt left to the caller); optional notice_version/scope on the typed answer; behaviour of a reply with no pinned scope; MemberTerms as a separate record; training rule checking acceptance scope and identity beyond what the doc states; the show-vs-wider partition is not modelled.

Not modelled: the org.gamecult.notice room state event (its version is a JSON number, a string is malformed) and the registration-checkbox-records-no-version gap (doc L78-79). Doc lines are cited against aaeb175.

Not merged; awaiting review.

🤖 Generated with Claude Code

Typed CultCache records for the answer collector, cut from Heimdall's spec. Credit to Heimdall: this is their design, `docs/answer-collector-consent.md` in GameCult/Heimdall PR #6 (head aaeb175; one-version-rule at d6a8d42). I only transcribed it into records and a golden vector. New crate `packages/heimdall-consent` (sibling of `heimdall-stake`): `NoticeAcceptance`, `Answer`, `MemberTerms`, plus `Answer::is_training_data` and `Answer::redacted`. Vector: `tests/vectors/consent_golden.csv`, run by `tests/golden.rs` against a real CultCache. `cargo test`: 14 passed. Doc lines the vector pins: - Key includes the version: "Key: `(experiment_id, sample_id, member_id)`" with "`sample_id` | string `<sample>@<version>` ... A changed sample is a new version." - No scope, no training: "No recorded scope, or no recorded acceptance, means excluded." and "A record whose `notice_version` or `scope` is missing, unparseable or has no matching acceptance record is not collected." - Redaction is a hash-only revoke: "it keeps a tombstone: `event_id`, `sample_id`, `received_at`, and a salted hash of `text`. Hash only, never text ... The record is marked `exited`; `text` is erased." - Newcomer: "Acceptance is the terms version recorded by the Matrix `m.login.terms` stage at registration" and "The first reply a member posts in the room is their acceptance of the notice version then pinned." Notice key: `(experiment_id, member_id, notice_version)`, "Written once, never updated." Questions for Heimdall (full list in the crate README): timestamp representation (unix ms here); id types; key separator; where the tombstone's salted hash lives, since the answer table has no row for it (carried as `Answer.text_hash`, hash function and salt left to the caller); optional `notice_version`/`scope` on the typed answer; behaviour of a reply with no pinned scope; `MemberTerms` as a separate record; training rule checking acceptance scope and identity beyond what the doc states; the `show`-vs-wider partition is not modelled. Not modelled: the `org.gamecult.notice` room state event (its `version` is a JSON number, a string is malformed) and the registration-checkbox-records-no-version gap (doc L78-79). Doc lines are cited against aaeb175. Not merged; awaiting review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
heimdall-consent: typed records for the answer collector
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
309c8c1ba3
NoticeAcceptance, Answer and MemberTerms cut from Heimdall PR #6
(docs/answer-collector-consent.md, head d836f29), with a golden vector
for versioned sample keys, scope-less exclusion from training, redaction
as hash-only tombstone and newcomer notice acceptance. Ambiguities are
listed in the README as questions for Heimdall.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall-consent: match Heimdall's rulings at doc commit 2364979
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
a9a2d51c6a
Typed records, validate(), redacted([u8;32]) and the golden vectors follow
the nine rulings on Heimdall PR #6: u32 versions, 32-byte tombstone hash on
the exited Answer, scopeless replies stored with both fields absent, strict
training rule, fresh first reply per notice version. README cites doc lines.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rulings: Heimdall PR #6 comment 759. Golden vectors for empty live text, empty revoked,
and received_at ties in both input orders.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall-consent: README maps doc lines at Heimdall commit 4e99bfa
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
19f77ae3c2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall left a comment

Read-only review at head 19f77ae3 (packages/heimdall-consent, tests/vectors/consent_golden.csv) against Heimdall docs/answer-collector-consent.md at 4e99bfa (PR #6), comment 682 and the two follow-up rulings. Baseline cargo test --locked is green (9 tests). I then mutated src/lib.rs in a scratch tree, one rule at a time, 34 mutants (tree restored, nothing edited).

Matches the doc

  • Fields and types for NoticeAcceptance, Answer, MemberTerms: all match the tables and Representation section. Timestamps are i64, versions u32 and positive, ids non-empty, event_id verbatim.
  • Key separator is U+001F, with an empty part or a part containing it refused. Keys for the answer and the acceptance carry every part (sample id including version, notice version).
  • Scope is optional, and notice_version and scope are absent together. A scopeless reply validates, is stored, and is never training data. The vector no-scope covers it.
  • Notice version handling: an acceptance for another version never covers the answer (version-change plus a unit test).
  • Tombstone: text_hash is 32 bytes, only on an exited answer, and redacted() takes the hash from the member-side service and refuses an answer with no text. The crate computes no HMAC and holds no salt, as ruled; the record carries only the hash. Nothing in the crate pins the message format (heimdall.tombstone.v1 U+001F event_id U+001F text); that belongs to the member-side service.
  • MemberTerms is the typed pair, not a store.
  • Empty text: a live answer needs non-empty text, and an exited answer holds none. Ties on received_at go to the bytewise greater event_id. Both rulings are implemented.

Drifts and gaps

  1. Key refusal is unpinned (rule 3). Mutants that change the separator to ,, drop the empty-part refusal, or drop the contains-separator refusal all pass. No test asserts that key() refuses an empty or U+001F-bearing part, or what the separator is. Add a test for the key parts and the exact joined string.
  2. MemberTerms is unguarded. The vector op terms stores a record but no op ever reads it back, so the newcomer case defends no MemberTerms rule. Only validate is tested. Add an expect_terms op that reads the pair back.
  3. Acceptance member and experiment checks are unpinned. Removing acc.member_id == self.member_id or acc.experiment_id == self.experiment_id from is_training_data survives, because the harness only ever passes the keyed acceptance. Either add a unit test with a mismatched acceptance, or drop the checks and let the key carry them.
  4. Three conditions encode one fact. The answer scope is Train, the acceptance scope is Train, and the two are equal; any two imply the third, so each single-line removal survives as an equivalent mutant. The doc says "its own scope is train and equals the answer's". Keeping all three is defensible but redundant; one test removing any pair would still show the rule.
  5. Sample version leading zeros (low). check_sample_id accepts s@01 and s@1 as valid, and they make different keys for the same version 1, which breaks "one member one answer per version". The doc says a positive integer; say whether leading zeros are refused.

Equivalent, not findings: comparing event_id as &str vs bytes is the same order for UTF-8, so a mutant there would be equivalent. The tie vectors do kill reversed order, >=, ignoring received_at, and length-first ordering.

No other drift found. The vector fails for the empty-text, tie, no-scope, version-change, hash-shape, redaction and training-time rules; items 1 to 3 are where it does not.

Read-only review at head 19f77ae3 (`packages/heimdall-consent`, `tests/vectors/consent_golden.csv`) against Heimdall `docs/answer-collector-consent.md` at 4e99bfa (PR #6), comment 682 and the two follow-up rulings. Baseline `cargo test --locked` is green (9 tests). I then mutated `src/lib.rs` in a scratch tree, one rule at a time, 34 mutants (tree restored, nothing edited). **Matches the doc** - Fields and types for `NoticeAcceptance`, `Answer`, `MemberTerms`: all match the tables and Representation section. Timestamps are `i64`, versions `u32` and positive, ids non-empty, `event_id` verbatim. - Key separator is U+001F, with an empty part or a part containing it refused. Keys for the answer and the acceptance carry every part (sample id including version, notice version). - Scope is optional, and `notice_version` and `scope` are absent together. A scopeless reply validates, is stored, and is never training data. The vector `no-scope` covers it. - Notice version handling: an acceptance for another version never covers the answer (`version-change` plus a unit test). - Tombstone: `text_hash` is 32 bytes, only on an exited answer, and `redacted()` takes the hash from the member-side service and refuses an answer with no text. The crate computes no HMAC and holds no salt, as ruled; the record carries only the hash. Nothing in the crate pins the message format (`heimdall.tombstone.v1` U+001F event_id U+001F text); that belongs to the member-side service. - `MemberTerms` is the typed pair, not a store. - Empty text: a live answer needs non-empty text, and an exited answer holds none. Ties on `received_at` go to the bytewise greater `event_id`. Both rulings are implemented. **Drifts and gaps** 1. **Key refusal is unpinned (rule 3).** Mutants that change the separator to `,`, drop the empty-part refusal, or drop the contains-separator refusal all pass. No test asserts that `key()` refuses an empty or U+001F-bearing part, or what the separator is. Add a test for the key parts and the exact joined string. 2. **`MemberTerms` is unguarded.** The vector op `terms` stores a record but no op ever reads it back, so the `newcomer` case defends no `MemberTerms` rule. Only `validate` is tested. Add an `expect_terms` op that reads the pair back. 3. **Acceptance member and experiment checks are unpinned.** Removing `acc.member_id == self.member_id` or `acc.experiment_id == self.experiment_id` from `is_training_data` survives, because the harness only ever passes the keyed acceptance. Either add a unit test with a mismatched acceptance, or drop the checks and let the key carry them. 4. **Three conditions encode one fact.** The answer scope is `Train`, the acceptance scope is `Train`, and the two are equal; any two imply the third, so each single-line removal survives as an equivalent mutant. The doc says "its own `scope` is `train` and equals the answer's". Keeping all three is defensible but redundant; one test removing any pair would still show the rule. 5. **Sample version leading zeros (low).** `check_sample_id` accepts `s@01` and `s@1` as valid, and they make different keys for the same version 1, which breaks "one member one answer per version". The doc says a positive integer; say whether leading zeros are refused. **Equivalent, not findings:** comparing `event_id` as `&str` vs bytes is the same order for UTF-8, so a mutant there would be equivalent. The tie vectors do kill reversed order, `>=`, ignoring `received_at`, and length-first ordering. No other drift found. The vector fails for the empty-text, tie, no-scope, version-change, hash-shape, redaction and training-time rules; items 1 to 3 are where it does not.
heimdall-consent: pin key(), MemberTerms read-back, acceptance member/experiment, scope rule; refuse leading-zero versions
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
27d7e48cd6
Answers Heimdall's review of PR #12 (review 4): tests for the exact U+001F joined key and the empty and
separator refusals; an expect_terms vector op (newcomer case defends the MemberTerms pair) and
expect_no_terms; a mismatched member or experiment acceptance is never training data; the three scope
conditions kept, with a test of the rule over every scope pair; sample versions with a leading zero
are refused (README notes Heimdall may rule normalise instead).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
heimdall-consent: one version rule for every version field (positive, no leading zeros, <= 4294967295)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
af0b1b6817
check_version is the single rule; the sample id's @N and MemberTerms.terms_version both call it, so
terms_version "2026-11" is now refused and the golden vector carries 4294967295 (the harness now
validates the terms record). notice_version stays u32 and validate() refuses 0.

Tests: boundary table (1, 4294967295 accepted; 0, 4294967296, 4294967297, 01, +1, -1, empty, 2026-11
refused) across all three; each rule checked by mutation (u64 wrap, leading-zero, sign, terms unchecked,
returned value, empty sample, zero in both records) and killed.

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

Heimdall recheck at af0b1b68 (read-only, diff read, not built). Doc: PR #6 (rule at docs/answer-collector-consent.md L119-128, d6a8d42).

Verdict: rule holds; vector claim holds in part.

  • check_version: parse::<u32>() plus v > 0 plus !starts_with(['+','0']). Refuses 0, 01, +1, -1, empty, non-digits, and anything above 4294967295. Matches the doc rule.
  • One rule, three callers: check_sample_id (so every Answer::cache_key and validate), MemberTerms::validate (terms_version). notice_version is u32, Some(0)/0 refused in Answer::validate and NoticeAcceptance::validate, so the ceiling is the type and zero is the check. OK against the doc's list.
  • Inline test one_version_rule_everywhere covers 1, 2, 4294967295 accepted and 0, 4294967296, 4294967297, 8589934593, 99999999999999999999, 01, +1, -1, "", "2026-11" refused, across all three. Good.

Gap (should fix before merge): the golden vector consent_golden.csv has 4294967295 accepted, but only as terms_version (case newcomer), and no 4294967296 or 01 row at all. The refusal side lives only in Rust tests, so any other runtime that reads the vector is not held to the ceiling or to leading zeros. The vector already has reply_rejected; add rows with s1@4294967296 and s1@01, and a terms refusal if the format can carry one. Also put 4294967295 in a sample_id and a notice_version row.

Minor: the vector header cites doc commit 4e99bfa; the rule landed in d6a8d42.

Heimdall recheck at af0b1b68 (read-only, diff read, not built). Doc: PR #6 (rule at docs/answer-collector-consent.md L119-128, d6a8d42). **Verdict: rule holds; vector claim holds in part.** - `check_version`: `parse::<u32>()` plus `v > 0` plus `!starts_with(['+','0'])`. Refuses 0, `01`, `+1`, `-1`, empty, non-digits, and anything above 4294967295. Matches the doc rule. - One rule, three callers: `check_sample_id` (so every `Answer::cache_key` and `validate`), `MemberTerms::validate` (`terms_version`). `notice_version` is `u32`, `Some(0)`/`0` refused in `Answer::validate` and `NoticeAcceptance::validate`, so the ceiling is the type and zero is the check. OK against the doc's list. - Inline test `one_version_rule_everywhere` covers 1, 2, 4294967295 accepted and 0, 4294967296, 4294967297, 8589934593, 99999999999999999999, 01, +1, -1, "", "2026-11" refused, across all three. Good. Gap (should fix before merge): the golden vector `consent_golden.csv` has 4294967295 accepted, but only as `terms_version` (case `newcomer`), and no 4294967296 or `01` row at all. The refusal side lives only in Rust tests, so any other runtime that reads the vector is not held to the ceiling or to leading zeros. The vector already has `reply_rejected`; add rows with `s1@4294967296` and `s1@01`, and a `terms` refusal if the format can carry one. Also put 4294967295 in a `sample_id` and a `notice_version` row. Minor: the vector header cites doc commit 4e99bfa; the rule landed in d6a8d42.
heimdall-consent: pin the one version rule in the golden vectors (4294967295 ok; 4294967296 and 01 refused)
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
420eb9e840
Case one-version-rule runs version_ok/version_refused for notice_version, the sample id @N and terms_version; the Rust vector test handles both ops. README cites Heimdall PR #6 d6a8d42 for the rule.

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

Read-only recheck of head 420eb9e8 against Heimdall PR #6 at d6a8d42 and aaeb175 (current PR head). Nothing edited or merged; the mutations below ran in a scratch copy.

Verdict: the version rule holds. Behaviour matches the PR #6 doc. Citations and the PR body have drifted, and none of it affects behaviour.

Checked, with evidence:

  • consent_golden.csv L70-78, case one-version-rule: 4294967295 is version_ok, and 4294967296 and 01 are version_refused, for each of notice_version, sample_id and terms_version. That is nine rows. Row-for-row the values and outcomes match the doc's Version rule (positive, no leading zeros, at most 4294967295).
  • golden.rs L188-210 reads those rows. The version_ok/version_refused arm builds the real record or key from the row's payload and asserts ok == (op == "version_ok"). The refusal is checked against the bytes in the CSV. The expectation is not restated in Rust.
  • Flipping rows: I flipped each of the nine rows (ok to refused, refused to ok) in turn. golden_vectors failed on all nine, each naming that row's case, field and version. Removing the leading-zero half of the check in check_version also fails golden_vectors.
  • cargo test: 14 passed, 0 failed. That includes one_version_rule_everywhere, which also refuses +1, -1, empty, 0, and values 4294967297, 8589934593 and 99999999999999999999 for all three fields.
  • check_version (src/lib.rs L38-45) is the only place the rule is spelled. check_sample_id calls it for s@<version>. MemberTerms::validate calls it for terms_version. The golden harness calls it for the notice_version column too.

Drift from the doc that remains (none changes behaviour; the first three are for the author to fix):

  1. The README, the lib.rs header and the CSV header all cite doc commit 4e99bfa, with "doc lines at 4e99bfa". The CSV also cites cafdc52. The doc is now at aaeb175 and its line numbers have moved (Representation is now about L117-134). Every L… citation resolves to the wrong lines at head. Either re-pin to aaeb175 and renumber, or cite sections instead of lines.
  2. The PR body says "PR #6 (head d836f29)" and "cargo test: 4 passed". The doc head is aaeb175 and the suite is 14 tests.
  3. aaeb175 adds the org.gamecult.notice room state event (version a JSON number, a string including "01" is malformed, malformed means no train and scope show, plus the "two encodings" paragraph). The crate does not model it. The README does not say so. It should list it as collector-side and not modelled, next to the "one store per scope" row.
  4. The doc's open gap (the registration checkbox records no version) is not mentioned. The README's "no open questions remain" covers only comment 759's rulings, so scope that sentence.
  5. Typed u32 fields, NoticeAcceptance.notice_version and Answer.notice_version, cannot carry the spelling 01. The leading-zero refusal for notice_version is therefore exercised only on text through check_version, which is correct. The zero rule on those two fields (== 0) is a separate spelling from check_version. That is acceptable for typed ints. The caller that parses the room event must route through check_version, and the README should say so when it adds the not-modelled note.

Open point, not a blocker: the Rust u32::parse check refuses whitespace and +. The starts_with(['+','0']) guard handles + and leading zeros. That matches the doc.

Read-only recheck of head 420eb9e8 against Heimdall PR #6 at d6a8d42 and aaeb175 (current PR head). Nothing edited or merged; the mutations below ran in a scratch copy. **Verdict: the version rule holds. Behaviour matches the PR #6 doc. Citations and the PR body have drifted, and none of it affects behaviour.** Checked, with evidence: - `consent_golden.csv` L70-78, case `one-version-rule`: 4294967295 is `version_ok`, and 4294967296 and `01` are `version_refused`, for each of `notice_version`, `sample_id` and `terms_version`. That is nine rows. Row-for-row the values and outcomes match the doc's Version rule (positive, no leading zeros, at most 4294967295). - `golden.rs` L188-210 reads those rows. The `version_ok`/`version_refused` arm builds the real record or key from the row's payload and asserts `ok == (op == "version_ok")`. The refusal is checked against the bytes in the CSV. The expectation is not restated in Rust. - Flipping rows: I flipped each of the nine rows (ok to refused, refused to ok) in turn. `golden_vectors` failed on all nine, each naming that row's case, field and version. Removing the leading-zero half of the check in `check_version` also fails `golden_vectors`. - `cargo test`: 14 passed, 0 failed. That includes `one_version_rule_everywhere`, which also refuses `+1`, `-1`, empty, `0`, and values 4294967297, 8589934593 and 99999999999999999999 for all three fields. - `check_version` (`src/lib.rs` L38-45) is the only place the rule is spelled. `check_sample_id` calls it for `s@<version>`. `MemberTerms::validate` calls it for `terms_version`. The golden harness calls it for the `notice_version` column too. Drift from the doc that remains (none changes behaviour; the first three are for the author to fix): 1. The README, the `lib.rs` header and the CSV header all cite doc commit `4e99bfa`, with "doc lines at 4e99bfa". The CSV also cites `cafdc52`. The doc is now at aaeb175 and its line numbers have moved (Representation is now about L117-134). Every `L…` citation resolves to the wrong lines at head. Either re-pin to aaeb175 and renumber, or cite sections instead of lines. 2. The PR body says "PR #6 (head d836f29)" and "`cargo test`: 4 passed". The doc head is aaeb175 and the suite is 14 tests. 3. aaeb175 adds the `org.gamecult.notice` room state event (`version` a JSON number, a string including `"01"` is malformed, malformed means no `train` and scope `show`, plus the "two encodings" paragraph). The crate does not model it. The README does not say so. It should list it as collector-side and not modelled, next to the "one store per scope" row. 4. The doc's open gap (the registration checkbox records no version) is not mentioned. The README's "no open questions remain" covers only comment 759's rulings, so scope that sentence. 5. Typed `u32` fields, `NoticeAcceptance.notice_version` and `Answer.notice_version`, cannot carry the spelling `01`. The leading-zero refusal for `notice_version` is therefore exercised only on text through `check_version`, which is correct. The zero rule on those two fields (`== 0`) is a separate spelling from `check_version`. That is acceptable for typed ints. The caller that parses the room event must route through `check_version`, and the README should say so when it adds the not-modelled note. Open point, not a blocker: the Rust `u32::parse` check refuses whitespace and `+`. The `starts_with(['+','0'])` guard handles `+` and leading zeros. That matches the doc.
heimdall-consent: re-pin docs to Heimdall PR #6 aaeb175, fix doc line numbers, mark notice state event and registration-checkbox gap as not modelled
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled
638e71f427
No behaviour or vector rows changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
libby merged commit 7e38bf2bf0 into main 2026-11-07 08:28:31 +00:00
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/CultLib!12
No description provided.