heimdall-consent: typed records for the answer collector #12
6 changed files with 1543 additions and 0 deletions
716
packages/heimdall-consent/Cargo.lock
generated
Normal file
716
packages/heimdall-consent/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "ar_archive_writer"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6"
|
||||
dependencies = [
|
||||
"object",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cultcache-rs"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"cultcache-rs-derive",
|
||||
"fs2",
|
||||
"redb",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"serde_stacker",
|
||||
"stacker",
|
||||
"uuid",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cultcache-rs-derive"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heimdall-consent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cultcache-rs",
|
||||
"serde",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.39.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622"
|
||||
dependencies = [
|
||||
"ar_archive_writer",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redb"
|
||||
version = "4.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b317ef681e18f6091efbfab532eaf924a135c492d9d0299d0fbfae07e92a39b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp-serde"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||
dependencies = [
|
||||
"rmp",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_bytes"
|
||||
version = "0.11.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_stacker"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
"stacker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
18
packages/heimdall-consent/Cargo.toml
Normal file
18
packages/heimdall-consent/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "heimdall-consent"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "heimdall_consent"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
cultcache-rs = { path = "../cultcache-rs" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
36
packages/heimdall-consent/README.md
Normal file
36
packages/heimdall-consent/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# heimdall-consent
|
||||
|
||||
Typed CultCache records for Heimdall's answer collector: `NoticeAcceptance`, `Answer` and `MemberTerms`,
|
||||
plus the rules that need no store to state (`Answer::is_training_data`, `Answer::redacted`,
|
||||
`validate`). Cut from `docs/answer-collector-consent.md` in GameCult/Heimdall PR #6 at doc commit
|
||||
`aaeb175` (PR head); the doc's tables and Representation section are the contract, and its rulings are on the PR
|
||||
(comments 682 and 759). Specification only: no collector, tally, export or tombstone hashing lives here.
|
||||
|
||||
Entry points: `src/lib.rs`; golden vectors `tests/vectors/consent_golden.csv`, run by `tests/golden.rs`
|
||||
(`cargo test`). Planned, not live: the collector, the tally, the training export.
|
||||
|
||||
## Where each ruling lands (doc lines at aaeb175)
|
||||
|
||||
| Ruling | Doc | Crate |
|
||||
|---|---|---|
|
||||
| Timestamps are Unix ms UTC, signed 64-bit | L118-119; `received_at` is collector-assigned, L95, L208-209 | `i64` on `accepted_at`, `received_at`, `terms_accepted_at` |
|
||||
| Ids are non-empty opaque strings, `event_id` verbatim; every version field (`notice_version`, the `@N` of a sample id, `terms_version`) is a positive integer, no leading zeros, no sign, at most 4294967295 (Heimdall ruling 2026-11-07, one rule; doc line "Version rule" under Representation, Heimdall PR #6 commit d6a8d42, now in the doc at aaeb175) | L120-136 | `check_version` is the single rule; `check_sample_id` and `MemberTerms::validate` call it; `notice_version: u32` with `validate()` refusing 0; golden vector `newcomer` carries `terms_version` 4294967295, and case `one-version-rule` pins 4294967295 accepted, 4294967296 and `01` refused for `notice_version`, the sample id `@N` and `terms_version` |
|
||||
| Key parts joined with U+001F; empty part or part containing it refused | L137-138 | `key()` |
|
||||
| Tombstone is the exited `Answer` with `text_hash` (32 bytes); HMAC-SHA-256 keyed by a per-member salt held by the member-side service; the collector cannot compute it | L94, L165-185, L221-226 | `Answer.text_hash`; `redacted([u8; 32])` takes the service's output and refuses an answer with no `text` (L181-182); `validate()` requires 32 bytes and `exited` |
|
||||
| A reply with no established notice version or scope is stored with both absent, writes no acceptance, is excluded from every use | L107-114 | `notice_version`/`scope` optional and absent together (`validate()`); vector case `no-scope` |
|
||||
| A reply's version is the one pinned at its `origin_server_ts`; changed version needs a fresh first reply | L19-20, L49-51, L112-114 | acceptance key includes the version; `is_training_data` accepts only the acceptance for the answer's own version; vector case `version-change` |
|
||||
| `MemberTerms` is the typed shape of the member record's `terms_version`/`terms_accepted_at`, not a second store | L68-76 | `MemberTerms` doc comment; the collector does not hold it |
|
||||
| Training needs acceptance `scope = train` equal to the answer's `scope`, and `accepted_at <= received_at` | L149-156 | `is_training_data` |
|
||||
| One store per scope plus a store for scopeless replies | L158-163 | not modelled; the collector's layout owns it |
|
||||
|
||||
Rulings of comment 759 (doc L92, L98-105; no open questions remain):
|
||||
|
||||
| Ruling | Doc | Crate |
|
||||
|---|---|---|
|
||||
| A live answer must have non-empty `text` and is rejected otherwise; empty text is valid only on an exited answer (text cleared, hash-only tombstone) | L92, L103-105 | `Answer::validate()` refuses a non-exited answer whose `text` is absent or empty; an exited answer holds no text. Vector cases `empty-live-rejected`, `empty-revoked` |
|
||||
| `received_at` is assigned by the collector; a tie on it breaks by `event_id` compared bytewise, the greater wins as the later reply | L95, L98-101 | `Answer::supersedes`: `(received_at, event_id bytes)` order; the vector harness replaces a stored answer only when the incoming one supersedes it. Vector cases `tie-greater-first`, `tie-greater-last` |
|
||||
|
||||
Not modelled (doc at aaeb175):
|
||||
|
||||
- The `org.gamecult.notice` room state event (L24-37, L132-136): its content is `{version, scope}` with `version` a JSON number, and a string (including `"01"`) is malformed. The crate checks the version only as decimal text (`check_version`); reading, power-level checks and the malformed-means-`show` fallback belong to the collector.
|
||||
- The registration-checkbox gap (L78-79): the `m.login.terms` checkbox records no version. The doc rules nothing on it; the crate takes `terms_version` as given.
|
||||
225
packages/heimdall-consent/src/lib.rs
Normal file
225
packages/heimdall-consent/src/lib.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
//! Typed CultCache records for Heimdall's answer collector, cut from `docs/answer-collector-consent.md`
|
||||
//! in GameCult/Heimdall PR #6 (doc commit aaeb175): the notice acceptance record, the answer record,
|
||||
//! and the typed shape of the terms pair on the member record. Field names and types follow the
|
||||
//! doc's tables and its Representation section; README.md cites the doc lines.
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use cultcache_rs::DatabaseEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// What a notice allows, each level including the ones below it. `Train` is declared in terms by the
|
||||
/// room's notice; nothing here widens a scope.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Scope {
|
||||
Show,
|
||||
Study,
|
||||
Train,
|
||||
}
|
||||
|
||||
/// Separates key parts (Representation: parts joined with U+001F; an empty part or one containing it
|
||||
/// is refused, not escaped).
|
||||
const SEP: char = '\u{1f}';
|
||||
|
||||
fn key(parts: &[&str]) -> Result<String> {
|
||||
if let Some(p) = parts.iter().find(|p| p.is_empty() || p.contains(SEP)) {
|
||||
bail!("key part {p:?} is empty or contains the key separator");
|
||||
}
|
||||
Ok(parts.join(&SEP.to_string()))
|
||||
}
|
||||
|
||||
fn nonempty(name: &str, value: &str) -> Result<()> {
|
||||
if value.is_empty() {
|
||||
bail!("{name} must be a non-empty string");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The one version rule (Heimdall ruling 2026-11-07): a positive integer written without leading
|
||||
/// zeros or a sign, at most 4294967295 (`u32::MAX`). `01` and `1` would be two keys for one version.
|
||||
pub fn check_version(version: &str) -> Result<u32> {
|
||||
match version.parse::<u32>() {
|
||||
Ok(v) if v > 0 && !version.starts_with(['+', '0']) => Ok(v),
|
||||
_ => bail!("version {version:?} must be a positive integer, no leading zeros, at most {}", u32::MAX),
|
||||
}
|
||||
}
|
||||
|
||||
/// `<sample>@<version>`, the version under `check_version`. The version is part of the id, so it is
|
||||
/// part of every answer key and no key can name a sample without one.
|
||||
pub fn check_sample_id(sample_id: &str) -> Result<(&str, u32)> {
|
||||
let Some((sample, version)) = sample_id.rsplit_once('@') else {
|
||||
bail!("sample id {sample_id:?} has no @version");
|
||||
};
|
||||
if sample.is_empty() {
|
||||
bail!("sample id {sample_id:?} needs a non-empty sample");
|
||||
}
|
||||
Ok((sample, check_version(version)?))
|
||||
}
|
||||
|
||||
/// The first reply a member posts under a notice version. Written once, never updated.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "heimdall.notice-acceptance", schema = "HeimdallNoticeAcceptance")]
|
||||
pub struct NoticeAcceptance {
|
||||
#[cultcache(key = 0)]
|
||||
pub member_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub experiment_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub notice_version: u32,
|
||||
#[cultcache(key = 3)]
|
||||
pub scope: Scope,
|
||||
/// `origin_server_ts` of the first reply, unix milliseconds UTC.
|
||||
#[cultcache(key = 4)]
|
||||
pub accepted_at: i64,
|
||||
#[cultcache(key = 5)]
|
||||
pub event_id: String,
|
||||
}
|
||||
|
||||
impl NoticeAcceptance {
|
||||
/// Representation: ids are non-empty strings, versions positive.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
nonempty("member_id", &self.member_id)?;
|
||||
nonempty("experiment_id", &self.experiment_id)?;
|
||||
nonempty("event_id", &self.event_id)?;
|
||||
if self.notice_version == 0 {
|
||||
bail!("notice_version must be positive");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `(experiment_id, member_id, notice_version)`.
|
||||
pub fn cache_key(experiment_id: &str, member_id: &str, notice_version: u32) -> Result<String> {
|
||||
key(&[experiment_id, member_id, ¬ice_version.to_string()])
|
||||
}
|
||||
}
|
||||
|
||||
/// One answer. `notice_version` and `scope` are absent together on a reply with no established
|
||||
/// notice version or no matching acceptance: such a reply is stored, excluded from every use, and
|
||||
/// writes no acceptance (Answer record, "A reply whose `notice_version` or `scope` is missing").
|
||||
/// Once exited the record is the tombstone: `text` absent, `text_hash` set.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "heimdall.answer", schema = "HeimdallAnswer")]
|
||||
pub struct Answer {
|
||||
#[cultcache(key = 0)]
|
||||
pub sample_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub experiment_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub member_id: String,
|
||||
#[cultcache(key = 3, default)]
|
||||
pub notice_version: Option<u32>,
|
||||
#[cultcache(key = 4, default)]
|
||||
pub scope: Option<Scope>,
|
||||
/// Absent once exited.
|
||||
#[cultcache(key = 5, default)]
|
||||
pub text: Option<String>,
|
||||
/// One-way: set by redaction, never cleared.
|
||||
#[cultcache(key = 6, default)]
|
||||
pub exited: bool,
|
||||
/// Assigned by the collector on receipt, unix milliseconds UTC, not copied from the reply (doc L95);
|
||||
/// ties are broken by `supersedes`.
|
||||
#[cultcache(key = 7)]
|
||||
pub received_at: i64,
|
||||
#[cultcache(key = 8)]
|
||||
pub event_id: String,
|
||||
/// The tombstone hash of `text`: HMAC-SHA-256, 32 raw bytes, computed by the member-side service
|
||||
/// (it holds the salt). Present only once exited, and only if the collector held `text`.
|
||||
#[cultcache(key = 9, default)]
|
||||
pub text_hash: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Answer {
|
||||
/// `(experiment_id, sample_id, member_id)`; refuses a sample id without a positive version.
|
||||
pub fn cache_key(experiment_id: &str, sample_id: &str, member_id: &str) -> Result<String> {
|
||||
check_sample_id(sample_id)?;
|
||||
key(&[experiment_id, sample_id, member_id])
|
||||
}
|
||||
|
||||
/// Representation and the invariants the table implies.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
nonempty("member_id", &self.member_id)?;
|
||||
nonempty("experiment_id", &self.experiment_id)?;
|
||||
nonempty("event_id", &self.event_id)?;
|
||||
check_sample_id(&self.sample_id)?;
|
||||
if self.notice_version == Some(0) {
|
||||
bail!("notice_version must be positive");
|
||||
}
|
||||
if self.notice_version.is_some() != self.scope.is_some() {
|
||||
bail!("notice_version and scope are present together or absent together");
|
||||
}
|
||||
if self.exited && self.text.is_some() {
|
||||
bail!("an exited answer holds no text");
|
||||
}
|
||||
if !self.exited && self.text.as_deref().is_none_or(str::is_empty) {
|
||||
bail!("a live answer needs non-empty text; empty text is valid only on a revoked answer");
|
||||
}
|
||||
if let Some(hash) = &self.text_hash {
|
||||
if !self.exited || hash.len() != 32 {
|
||||
bail!("text_hash is 32 bytes and only on an exited answer");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replacement rule for two answers at one key: the later reply is the greater `(received_at,
|
||||
/// event_id)`. `received_at` is assigned by the collector; a tie breaks by `event_id` compared
|
||||
/// bytewise, the greater winning (Heimdall PR #6 comment 759). Insertion order never decides.
|
||||
pub fn supersedes(&self, earlier: &Answer) -> bool {
|
||||
(self.received_at, self.event_id.as_bytes()) > (earlier.received_at, earlier.event_id.as_bytes())
|
||||
}
|
||||
|
||||
/// Redaction is exit: text erased, `exited` set, the tombstone hash kept. The hash comes from the
|
||||
/// member-side service, which holds the salt; this crate never computes it. A reply the collector
|
||||
/// never collected has no text and gets no tombstone, so an answer without `text` is refused.
|
||||
pub fn redacted(&self, text_hash: [u8; 32]) -> Result<Answer> {
|
||||
if self.text.is_none() {
|
||||
bail!("no text held for event {:?}: nothing to tombstone", self.event_id);
|
||||
}
|
||||
Ok(Answer { text: None, exited: true, text_hash: Some(text_hash.to_vec()), ..self.clone() })
|
||||
}
|
||||
|
||||
/// Training rule (doc "Training rule"): scope `train`; an acceptance for this answer's
|
||||
/// `(experiment_id, member_id, notice_version)` whose own scope is `train` and equals the answer's,
|
||||
/// with `accepted_at <= received_at`; not exited. A changed notice version needs its own acceptance,
|
||||
/// so an acceptance for another version never covers the answer. No recorded scope, version or
|
||||
/// acceptance means excluded.
|
||||
pub fn is_training_data(&self, acceptance: Option<&NoticeAcceptance>) -> bool {
|
||||
let (Some(Scope::Train), Some(version), Some(acc)) = (self.scope, self.notice_version, acceptance) else {
|
||||
return false;
|
||||
};
|
||||
!self.exited
|
||||
&& acc.notice_version == version
|
||||
&& acc.experiment_id == self.experiment_id
|
||||
&& acc.member_id == self.member_id
|
||||
&& acc.scope == Scope::Train
|
||||
&& Some(acc.scope) == self.scope
|
||||
&& acc.accepted_at <= self.received_at
|
||||
}
|
||||
}
|
||||
|
||||
/// The typed shape of the `terms_version` / `terms_accepted_at` pair on the Heimdall member record
|
||||
/// (newcomers only, recorded by the `m.login.terms` stage). It is not a second store and the
|
||||
/// collector does not hold it. Members who joined before the stage, and Minds, have no such pair.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "heimdall.member-terms", schema = "HeimdallMemberTerms")]
|
||||
pub struct MemberTerms {
|
||||
#[cultcache(key = 0)]
|
||||
pub member_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub terms_version: String,
|
||||
/// Unix milliseconds UTC.
|
||||
#[cultcache(key = 2)]
|
||||
pub terms_accepted_at: i64,
|
||||
}
|
||||
|
||||
impl MemberTerms {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
nonempty("member_id", &self.member_id)?;
|
||||
check_version(&self.terms_version).map(drop)
|
||||
}
|
||||
|
||||
/// `member_id`.
|
||||
pub fn cache_key(member_id: &str) -> Result<String> {
|
||||
key(&[member_id])
|
||||
}
|
||||
}
|
||||
470
packages/heimdall-consent/tests/golden.rs
Normal file
470
packages/heimdall-consent/tests/golden.rs
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
use cultcache_rs::{CultCache, SingleFileMessagePackBackingStore};
|
||||
use heimdall_consent::*;
|
||||
|
||||
const VECTORS: &str = include_str!("vectors/consent_golden.csv");
|
||||
|
||||
fn new_cache(dir: &tempfile::TempDir) -> CultCache {
|
||||
let mut cache = CultCache::new();
|
||||
cache.register_entry_type::<NoticeAcceptance>().unwrap();
|
||||
cache.register_entry_type::<Answer>().unwrap();
|
||||
cache.register_entry_type::<MemberTerms>().unwrap();
|
||||
let _ = cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(dir.path().join("consent.cc")));
|
||||
cache.pull_all_backing_stores().unwrap();
|
||||
cache
|
||||
}
|
||||
|
||||
fn scope(s: &str) -> Option<Scope> {
|
||||
match s {
|
||||
"" => None,
|
||||
"show" => Some(Scope::Show),
|
||||
"study" => Some(Scope::Study),
|
||||
"train" => Some(Scope::Train),
|
||||
other => panic!("unknown scope {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn hex(s: &str) -> Vec<u8> {
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||||
}
|
||||
|
||||
fn put_answer(cache: &mut CultCache, a: &Answer) {
|
||||
cache.put(Answer::cache_key(&a.experiment_id, &a.sample_id, &a.member_id).unwrap(), a).unwrap();
|
||||
}
|
||||
|
||||
/// The collector's replace rule: an incoming answer replaces the stored one only if it is the later reply.
|
||||
fn put_if_later(cache: &mut CultCache, a: &Answer) {
|
||||
let k = Answer::cache_key(&a.experiment_id, &a.sample_id, &a.member_id).unwrap();
|
||||
if cache.get::<Answer>(&k).unwrap().is_some_and(|old| !a.supersedes(&old)) {
|
||||
return;
|
||||
}
|
||||
cache.put(k, a).unwrap();
|
||||
}
|
||||
|
||||
fn training_events(cache: &CultCache) -> Vec<String> {
|
||||
let mut ids: Vec<String> = cache
|
||||
.get_all::<Answer>()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
let acc = a.notice_version.and_then(|v| {
|
||||
cache.get::<NoticeAcceptance>(&NoticeAcceptance::cache_key(&a.experiment_id, &a.member_id, v).unwrap()).unwrap()
|
||||
});
|
||||
a.is_training_data(acc.as_ref())
|
||||
})
|
||||
.map(|a| a.event_id.clone())
|
||||
.collect();
|
||||
ids.sort();
|
||||
ids
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_vectors() {
|
||||
let mut cases: Vec<(&str, Vec<Vec<&str>>)> = Vec::new();
|
||||
for line in VECTORS.lines().filter(|l| !l.starts_with('#') && !l.trim().is_empty()) {
|
||||
let f: Vec<&str> = line.split(',').collect();
|
||||
assert_eq!(f.len(), 10, "row `{line}`");
|
||||
match cases.last_mut() {
|
||||
Some((name, rows)) if *name == f[0] => rows.push(f),
|
||||
_ => cases.push((f[0], vec![f])),
|
||||
}
|
||||
}
|
||||
assert_eq!(cases.len(), 10, "fixture shrank");
|
||||
for (name, rows) in cases {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut cache = new_cache(&dir);
|
||||
for f in rows {
|
||||
let (op, member, room, nv, sc, sample, payload, ts, event) = (f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]);
|
||||
let ctx = format!("case {name} op {op} event {event}");
|
||||
let ts: i64 = ts.parse().unwrap_or(0);
|
||||
let nv: Option<u32> = nv.parse().ok();
|
||||
match op {
|
||||
"reply" | "answer_raw" => {
|
||||
if op == "reply" {
|
||||
if let (Some(v), Some(s)) = (nv, scope(sc)) {
|
||||
let k = NoticeAcceptance::cache_key(room, member, v).unwrap();
|
||||
if cache.get::<NoticeAcceptance>(&k).unwrap().is_none() {
|
||||
cache
|
||||
.put(k, &NoticeAcceptance {
|
||||
member_id: member.into(),
|
||||
experiment_id: room.into(),
|
||||
notice_version: v,
|
||||
scope: s,
|
||||
accepted_at: ts,
|
||||
event_id: event.into(),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
let answer = Answer {
|
||||
sample_id: sample.into(),
|
||||
experiment_id: room.into(),
|
||||
member_id: member.into(),
|
||||
notice_version: nv,
|
||||
scope: scope(sc),
|
||||
text: Some(payload.into()),
|
||||
exited: false,
|
||||
received_at: ts,
|
||||
event_id: event.into(),
|
||||
text_hash: None,
|
||||
};
|
||||
answer.validate().unwrap();
|
||||
put_if_later(&mut cache, &answer);
|
||||
}
|
||||
"reply_rejected" => {
|
||||
let answer = Answer {
|
||||
sample_id: sample.into(),
|
||||
experiment_id: room.into(),
|
||||
member_id: member.into(),
|
||||
notice_version: nv,
|
||||
scope: scope(sc),
|
||||
text: Some(payload.into()),
|
||||
exited: false,
|
||||
received_at: ts,
|
||||
event_id: event.into(),
|
||||
text_hash: None,
|
||||
};
|
||||
assert!(answer.validate().is_err(), "{ctx}: must be refused");
|
||||
}
|
||||
"answer_revoked" => {
|
||||
let answer = Answer {
|
||||
sample_id: sample.into(),
|
||||
experiment_id: room.into(),
|
||||
member_id: member.into(),
|
||||
notice_version: nv,
|
||||
scope: scope(sc),
|
||||
text: None,
|
||||
exited: true,
|
||||
received_at: ts,
|
||||
event_id: event.into(),
|
||||
text_hash: if payload.is_empty() { None } else { Some(hex(payload)) },
|
||||
};
|
||||
answer.validate().unwrap();
|
||||
put_if_later(&mut cache, &answer);
|
||||
}
|
||||
"expect_winner" => {
|
||||
let a = cache.get_required::<Answer>(&Answer::cache_key(room, sample, member).unwrap()).unwrap();
|
||||
assert_eq!((a.event_id.as_str(), a.text.as_deref()), (event, Some(payload)), "{ctx}");
|
||||
}
|
||||
"redact" => {
|
||||
let k = Answer::cache_key(room, sample, member).unwrap();
|
||||
let a = cache.get_required::<Answer>(&k).unwrap();
|
||||
assert_eq!(a.event_id, event, "{ctx}");
|
||||
let hash: [u8; 32] = hex(payload).try_into().expect("tombstone hash is 32 bytes");
|
||||
put_answer(&mut cache, &a.redacted(hash).unwrap());
|
||||
}
|
||||
"terms" => {
|
||||
let terms = MemberTerms { member_id: member.into(), terms_version: payload.into(), terms_accepted_at: ts };
|
||||
terms.validate().unwrap();
|
||||
cache.put(MemberTerms::cache_key(member).unwrap(), &terms).unwrap();
|
||||
}
|
||||
"expect_terms" => {
|
||||
let t = cache.get_required::<MemberTerms>(&MemberTerms::cache_key(member).unwrap()).unwrap();
|
||||
assert_eq!((t.member_id.as_str(), t.terms_version.as_str(), t.terms_accepted_at), (member, payload, ts), "{ctx}");
|
||||
}
|
||||
"expect_no_terms" => assert!(cache.get::<MemberTerms>(&MemberTerms::cache_key(member).unwrap()).unwrap().is_none(), "{ctx}"),
|
||||
"expect_counts" => {
|
||||
let want = format!(
|
||||
"answers={};acceptances={}",
|
||||
cache.get_all::<Answer>().unwrap().len(),
|
||||
cache.get_all::<NoticeAcceptance>().unwrap().len()
|
||||
);
|
||||
assert_eq!(want, payload, "{ctx}");
|
||||
}
|
||||
"expect_train" => assert_eq!(training_events(&cache).join(";"), payload, "{ctx}"),
|
||||
"expect_exited" => {
|
||||
let a = cache.get_required::<Answer>(&Answer::cache_key(room, sample, member).unwrap()).unwrap();
|
||||
assert!(a.exited && a.text.is_none(), "{ctx}");
|
||||
assert_eq!(a.text_hash, Some(hex(payload)), "{ctx}");
|
||||
assert_eq!((a.event_id.as_str(), a.received_at, a.sample_id.as_str()), (event, ts, sample), "{ctx}");
|
||||
}
|
||||
"expect_acceptance" => {
|
||||
let a = cache
|
||||
.get_required::<NoticeAcceptance>(&NoticeAcceptance::cache_key(room, member, nv.unwrap()).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!((a.notice_version, Some(a.scope), a.accepted_at), (nv.unwrap(), scope(sc), ts), "{ctx}");
|
||||
assert_eq!(a.event_id, payload, "{ctx}");
|
||||
}
|
||||
"version_ok" | "version_refused" => {
|
||||
// member column names the field; payload is the version text as it arrives
|
||||
let ok = match member {
|
||||
"notice_version" => check_version(payload).is_ok_and(|v| {
|
||||
NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: room.into(),
|
||||
notice_version: v,
|
||||
scope: Scope::Train,
|
||||
accepted_at: 1,
|
||||
event_id: "e".into(),
|
||||
}
|
||||
.validate()
|
||||
.is_ok()
|
||||
}),
|
||||
"sample_id" => check_sample_id(&format!("s@{payload}")).is_ok(),
|
||||
"terms_version" => {
|
||||
MemberTerms { member_id: "m".into(), terms_version: payload.into(), terms_accepted_at: 1 }.validate().is_ok()
|
||||
}
|
||||
other => panic!("unknown version field {other}"),
|
||||
};
|
||||
assert_eq!(ok, op == "version_ok", "{ctx} field {member} version {payload}");
|
||||
}
|
||||
other => panic!("unknown op {other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_survive_a_reopen() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut cache = new_cache(&dir);
|
||||
let a = Answer {
|
||||
sample_id: "s@1".into(),
|
||||
experiment_id: "r".into(),
|
||||
member_id: "m".into(),
|
||||
notice_version: Some(2),
|
||||
scope: Some(Scope::Train),
|
||||
text: None,
|
||||
exited: true,
|
||||
received_at: 5,
|
||||
event_id: "e".into(),
|
||||
text_hash: Some(vec![7; 32]),
|
||||
};
|
||||
a.validate().unwrap();
|
||||
put_answer(&mut cache, &a);
|
||||
let reopened = new_cache(&dir);
|
||||
assert_eq!(reopened.get_required::<Answer>(&Answer::cache_key("r", "s@1", "m").unwrap()).unwrap(), a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_refuses_unversioned_sample_ids() {
|
||||
for bad in ["s", "s@", "s@0", "s@x", "@1", "s@-1", "s@+1", "s@4294967296", "s@01", "s@00", "s@001"] {
|
||||
assert!(Answer::cache_key("r", bad, "m").is_err(), "{bad}");
|
||||
}
|
||||
assert!(Answer::cache_key("r", "s@4294967295", "m").is_ok());
|
||||
assert!(Answer::cache_key("r", "s@1", "m").unwrap() != Answer::cache_key("r", "s@2", "m").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_version_rule_everywhere() {
|
||||
for ok in ["1", "2", "4294967295"] {
|
||||
let n: u32 = ok.parse().unwrap();
|
||||
assert_eq!(check_version(ok).unwrap(), n, "{ok}");
|
||||
assert_eq!(check_sample_id(&format!("s@{ok}")).unwrap(), ("s", n), "s@{ok}");
|
||||
assert!(MemberTerms { member_id: "m".into(), terms_version: ok.into(), terms_accepted_at: 1 }.validate().is_ok(), "terms {ok}");
|
||||
}
|
||||
for bad in ["0", "4294967296", "4294967297", "8589934593", "99999999999999999999", "01", "+1", "-1", "", "2026-11"] {
|
||||
assert!(check_version(bad).is_err(), "{bad}");
|
||||
assert!(check_sample_id(&format!("s@{bad}")).is_err(), "s@{bad}");
|
||||
assert!(MemberTerms { member_id: "m".into(), terms_version: bad.into(), terms_accepted_at: 1 }.validate().is_err(), "terms {bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notice_version_zero_is_refused_in_both_records() {
|
||||
let acc = NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 0,
|
||||
scope: Scope::Train,
|
||||
accepted_at: 1,
|
||||
event_id: "e".into(),
|
||||
};
|
||||
assert!(acc.validate().is_err());
|
||||
assert!(NoticeAcceptance { notice_version: u32::MAX, ..acc }.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acceptance_after_the_answer_does_not_cover_it() {
|
||||
let a = Answer {
|
||||
sample_id: "s@1".into(),
|
||||
experiment_id: "r".into(),
|
||||
member_id: "m".into(),
|
||||
notice_version: Some(1),
|
||||
scope: Some(Scope::Train),
|
||||
text: Some("t".into()),
|
||||
exited: false,
|
||||
received_at: 100,
|
||||
event_id: "e".into(),
|
||||
text_hash: None,
|
||||
};
|
||||
let acc = |at| NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 1,
|
||||
scope: Scope::Train,
|
||||
accepted_at: at,
|
||||
event_id: "e".into(),
|
||||
};
|
||||
assert!(a.is_training_data(Some(&acc(100))));
|
||||
assert!(!a.is_training_data(Some(&acc(101))));
|
||||
assert!(!a.is_training_data(None));
|
||||
}
|
||||
|
||||
fn answer() -> Answer {
|
||||
Answer {
|
||||
sample_id: "s@1".into(),
|
||||
experiment_id: "r".into(),
|
||||
member_id: "m".into(),
|
||||
notice_version: Some(1),
|
||||
scope: Some(Scope::Train),
|
||||
text: Some("t".into()),
|
||||
exited: false,
|
||||
received_at: 100,
|
||||
event_id: "e".into(),
|
||||
text_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acceptance_for_another_notice_version_never_covers_the_answer() {
|
||||
let acc = NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 1,
|
||||
scope: Scope::Train,
|
||||
accepted_at: 1,
|
||||
event_id: "e0".into(),
|
||||
};
|
||||
let mut a = answer();
|
||||
assert!(a.is_training_data(Some(&acc)));
|
||||
a.notice_version = Some(2);
|
||||
assert!(!a.is_training_data(Some(&acc)), "a changed notice version needs a fresh first reply");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn training_needs_acceptance_scope_equal_to_the_answers_scope() {
|
||||
let mut acc = NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 1,
|
||||
scope: Scope::Study,
|
||||
accepted_at: 1,
|
||||
event_id: "e0".into(),
|
||||
};
|
||||
assert!(!answer().is_training_data(Some(&acc)));
|
||||
acc.scope = Scope::Train;
|
||||
assert!(answer().is_training_data(Some(&acc)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reply_never_collected_gets_no_tombstone() {
|
||||
let mut a = answer();
|
||||
a.text = None;
|
||||
assert!(a.redacted([0; 32]).is_err());
|
||||
let t = answer().redacted([9; 32]).unwrap();
|
||||
assert!(t.exited && t.text.is_none() && t.text_hash == Some(vec![9; 32]));
|
||||
assert!(t.redacted([9; 32]).is_err(), "exit is one-way and a tombstone has no text to hash again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_enforces_the_representation_rules() {
|
||||
assert!(answer().validate().is_ok());
|
||||
let cases: Vec<(&str, Box<dyn Fn(&mut Answer)>)> = vec![
|
||||
("empty member", Box::new(|a| a.member_id.clear())),
|
||||
("empty event", Box::new(|a| a.event_id.clear())),
|
||||
("version zero", Box::new(|a| a.notice_version = Some(0))),
|
||||
("version without scope", Box::new(|a| a.scope = None)),
|
||||
("scope without version", Box::new(|a| a.notice_version = None)),
|
||||
("live without text", Box::new(|a| a.text = None)),
|
||||
("live with empty text", Box::new(|a| a.text = Some(String::new()))),
|
||||
("exited with text", Box::new(|a| a.exited = true)),
|
||||
("hash on a live answer", Box::new(|a| a.text_hash = Some(vec![0; 32]))),
|
||||
("short hash", Box::new(|a| {
|
||||
a.exited = true;
|
||||
a.text = None;
|
||||
a.text_hash = Some(vec![0; 31]);
|
||||
})),
|
||||
];
|
||||
for (name, break_it) in cases {
|
||||
let mut a = answer();
|
||||
break_it(&mut a);
|
||||
assert!(a.validate().is_err(), "{name}");
|
||||
}
|
||||
let mut revoked = answer();
|
||||
revoked.text = None;
|
||||
revoked.exited = true;
|
||||
assert!(revoked.validate().is_ok(), "empty text is valid on a revoked answer");
|
||||
let mut unscoped = answer();
|
||||
unscoped.notice_version = None;
|
||||
unscoped.scope = None;
|
||||
assert!(unscoped.validate().is_ok(), "a scopeless reply is stored");
|
||||
let acc = NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 0,
|
||||
scope: Scope::Show,
|
||||
accepted_at: 1,
|
||||
event_id: "e".into(),
|
||||
};
|
||||
assert!(acc.validate().is_err());
|
||||
assert!(MemberTerms { member_id: "m".into(), terms_version: String::new(), terms_accepted_at: 1 }.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_reply_is_greater_received_at_then_bytewise_event_id() {
|
||||
let at = |t, e: &str| Answer { received_at: t, event_id: e.into(), ..answer() };
|
||||
assert!(at(2, "a").supersedes(&at(1, "z")));
|
||||
assert!(!at(1, "z").supersedes(&at(2, "a")));
|
||||
assert!(at(1, "a").supersedes(&at(1, "B")), "bytewise: 0x61 > 0x42");
|
||||
assert!(at(1, "z").supersedes(&at(1, "aa")), "bytewise, not by length");
|
||||
assert!(!at(1, "e").supersedes(&at(1, "e")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_join_the_parts_with_u001f_and_refuse_empty_or_separator_parts() {
|
||||
assert_eq!(Answer::cache_key("r", "s@1", "m").unwrap(), "r\u{1f}s@1\u{1f}m");
|
||||
assert_eq!(NoticeAcceptance::cache_key("r", "m", 3).unwrap(), "r\u{1f}m\u{1f}3");
|
||||
assert_eq!(MemberTerms::cache_key("m").unwrap(), "m");
|
||||
for (experiment, sample, member) in [("", "s@1", "m"), ("r", "s@1", ""), ("r\u{1f}x", "s@1", "m"), ("r", "s\u{1f}x@1", "m"), ("r", "s@1", "m\u{1f}x")] {
|
||||
assert!(Answer::cache_key(experiment, sample, member).is_err(), "{experiment:?} {sample:?} {member:?}");
|
||||
}
|
||||
assert!(NoticeAcceptance::cache_key("", "m", 1).is_err());
|
||||
assert!(NoticeAcceptance::cache_key("r", "", 1).is_err());
|
||||
assert!(NoticeAcceptance::cache_key("r\u{1f}", "m", 1).is_err());
|
||||
assert!(NoticeAcceptance::cache_key("r", "m\u{1f}", 1).is_err());
|
||||
assert!(MemberTerms::cache_key("").is_err());
|
||||
assert!(MemberTerms::cache_key("m\u{1f}").is_err());
|
||||
// Parts that would collide if the separator were not refused or were `,` or empty.
|
||||
assert_ne!(Answer::cache_key("a", "s@1", "bc").unwrap(), Answer::cache_key("ab", "s@1", "c").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_acceptance_for_another_member_or_experiment_never_covers_the_answer() {
|
||||
let acc = NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 1,
|
||||
scope: Scope::Train,
|
||||
accepted_at: 1,
|
||||
event_id: "e0".into(),
|
||||
};
|
||||
assert!(answer().is_training_data(Some(&acc)));
|
||||
let other_member = NoticeAcceptance { member_id: "m2".into(), ..acc.clone() };
|
||||
let other_experiment = NoticeAcceptance { experiment_id: "r2".into(), ..acc };
|
||||
assert!(!answer().is_training_data(Some(&other_member)));
|
||||
assert!(!answer().is_training_data(Some(&other_experiment)));
|
||||
}
|
||||
|
||||
/// The doc states the rule once: the answer's scope is `train`, the acceptance's own scope is `train`,
|
||||
/// and the two are equal. The code keeps all three conditions (any two imply the third, so a single
|
||||
/// removal is an equivalent mutant); this pins the rule itself over every pair of scopes.
|
||||
#[test]
|
||||
fn only_a_train_answer_under_a_train_acceptance_is_training_data() {
|
||||
let scopes = [Scope::Show, Scope::Study, Scope::Train];
|
||||
for answer_scope in scopes {
|
||||
for acceptance_scope in scopes {
|
||||
let a = Answer { scope: Some(answer_scope), ..answer() };
|
||||
let acc = NoticeAcceptance {
|
||||
member_id: "m".into(),
|
||||
experiment_id: "r".into(),
|
||||
notice_version: 1,
|
||||
scope: acceptance_scope,
|
||||
accepted_at: 1,
|
||||
event_id: "e0".into(),
|
||||
};
|
||||
let want = answer_scope == Scope::Train && acceptance_scope == Scope::Train;
|
||||
assert_eq!(a.is_training_data(Some(&acc)), want, "answer {answer_scope:?} under acceptance {acceptance_scope:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
78
packages/heimdall-consent/tests/vectors/consent_golden.csv
Normal file
78
packages/heimdall-consent/tests/vectors/consent_golden.csv
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# case,op,member,experiment,notice_version,scope,sample_id,payload,ts,event_id
|
||||
# Ops run in order against a real CultCache; each case starts empty. Timestamps are unix ms.
|
||||
# reply: first reply under a (room, member, notice_version) writes an acceptance, then the answer replaces any earlier one at (room, sample_id, member); payload is the text.
|
||||
# answer_raw: stores the answer with no acceptance written. redact: replaces the answer whose event_id matches with its tombstone; payload is the 32-byte tombstone hash as 64 hex chars (the member-side service's output; the crate never computes it).
|
||||
# A reply with no established notice version or scope is stored with both absent (doc L107-114, Answer record), writes no acceptance, and is excluded. answer_raw with a version but no acceptance models a lost or never-written acceptance: still excluded (doc L152-158).
|
||||
# reply_rejected: validate must refuse the answer (nothing stored). answer_revoked: stores an exited answer with no text; payload is the tombstone hash (empty means none). expect_winner: the stored answer's event_id and text must equal event and payload.
|
||||
# Replacement at one key: the later reply is the greater (received_at, event_id), event_id compared bytewise; input order never decides (doc L98-101 and Heimdall PR #6 comment 759).
|
||||
# terms: member field terms_version (payload) and terms_accepted_at (ts). expect_terms: the stored pair for that member reads back as payload and ts; expect_no_terms: the member has none. expect_*: payload is what must hold.
|
||||
# version_ok / version_refused: the member column names the field (notice_version, sample_id, terms_version), payload is the version text; a sample id is checked as s@<payload>. One rule for all three: positive integer, no leading zeros, at most 4294967295 (Heimdall PR #6 d6a8d42, doc at aaeb175).
|
||||
# Expected values are written by hand from docs/answer-collector-consent.md (Heimdall PR #6, doc commit aaeb175), not computed by the crate.
|
||||
versioned-key,reply,m1,room1,1,train,s1@1,first,100,e1
|
||||
versioned-key,reply,m1,room1,1,train,s1@2,second,200,e2
|
||||
versioned-key,reply,m1,room1,1,train,s1@1,replaced,300,e3
|
||||
versioned-key,expect_counts,,,,,,answers=2;acceptances=1,,
|
||||
versioned-key,expect_train,,,,,,e2;e3,,
|
||||
no-scope,reply,m2,room1,,,s1@1,unscoped,100,e4
|
||||
no-scope,reply,m2,room1,,,s1@2,unversioned,110,e5
|
||||
no-scope,answer_raw,m3,room1,1,train,s1@1,no-acceptance,120,e6
|
||||
no-scope,reply,m4,room1,1,study,s1@1,study-only,130,e7
|
||||
no-scope,reply,m5,room1,1,train,s1@1,trainable,140,e8
|
||||
no-scope,expect_counts,,,,,,answers=5;acceptances=2,,
|
||||
no-scope,expect_train,,,,,,e8,,
|
||||
redacted-reply,reply,m6,room1,1,train,s1@1,secret words,100,e9
|
||||
redacted-reply,reply,m7,room1,1,train,s1@1,kept,110,e10
|
||||
redacted-reply,redact,m6,room1,,,s1@1,a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90,,e9
|
||||
redacted-reply,expect_exited,m6,room1,,,s1@1,a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90,100,e9
|
||||
redacted-reply,expect_train,,,,,,e10,,
|
||||
redacted-reply,expect_counts,,,,,,answers=2;acceptances=2,,
|
||||
newcomer,terms,m8,,,,,4294967295,50,
|
||||
newcomer,expect_terms,m8,,,,,4294967295,50,
|
||||
newcomer,expect_no_terms,m9,,,,,,,
|
||||
newcomer,reply,m8,room1,3,train,s1@1,hello,100,e11
|
||||
newcomer,reply,m9,room1,3,train,s1@1,veteran,110,e12
|
||||
newcomer,reply,m8,room1,4,study,s1@1,again,200,e13
|
||||
newcomer,expect_acceptance,m8,room1,3,train,,e11,100,
|
||||
newcomer,expect_acceptance,m8,room1,4,study,,e13,200,
|
||||
newcomer,expect_acceptance,m9,room1,3,train,,e12,110,
|
||||
newcomer,expect_counts,,,,,,answers=2;acceptances=3,,
|
||||
newcomer,expect_train,,,,,,e12,,
|
||||
version-change,reply,m10,room1,1,train,s1@1,under v1,100,e14
|
||||
version-change,answer_raw,m10,room1,2,train,s1@2,v2 without a fresh first reply,200,e15
|
||||
version-change,expect_counts,,,,,,answers=2;acceptances=1,,
|
||||
version-change,expect_train,,,,,,e14,,
|
||||
version-change,reply,m10,room1,2,train,s1@3,first reply under v2,300,e16
|
||||
version-change,expect_counts,,,,,,answers=3;acceptances=2,,
|
||||
version-change,expect_train,,,,,,e14;e16,,
|
||||
empty-live-rejected,reply_rejected,m11,room1,,,s1@1,,100,e17
|
||||
empty-live-rejected,expect_counts,,,,,,answers=0;acceptances=0,,
|
||||
empty-live-rejected,reply,m11,room1,,,s1@1,now filled,110,e18
|
||||
empty-live-rejected,expect_winner,m11,room1,,,s1@1,now filled,,e18
|
||||
empty-revoked,answer_revoked,m12,room1,1,train,s1@1,,100,e19
|
||||
empty-revoked,answer_revoked,m13,room1,1,train,s1@1,a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90,100,e20
|
||||
empty-revoked,expect_counts,,,,,,answers=2;acceptances=0,,
|
||||
empty-revoked,expect_train,,,,,,,,
|
||||
tie-greater-first,reply,m14,room1,,,s1@1,upper-B,500,B
|
||||
tie-greater-first,reply,m14,room1,,,s1@1,lower-a,500,a
|
||||
tie-greater-first,reply,m15,room1,,,s1@1,short-z,500,z
|
||||
tie-greater-first,reply,m15,room1,,,s1@1,long-aa,500,aa
|
||||
tie-greater-first,reply,m16,room1,,,s1@1,newer-ts-first,600,a
|
||||
tie-greater-first,reply,m16,room1,,,s1@1,older-ts-last,500,z
|
||||
tie-greater-first,expect_winner,m14,room1,,,s1@1,lower-a,,a
|
||||
tie-greater-first,expect_winner,m15,room1,,,s1@1,short-z,,z
|
||||
tie-greater-first,expect_winner,m16,room1,,,s1@1,newer-ts-first,,a
|
||||
tie-greater-last,reply,m14,room1,,,s1@1,lower-a,500,a
|
||||
tie-greater-last,reply,m14,room1,,,s1@1,upper-B,500,B
|
||||
tie-greater-last,reply,m15,room1,,,s1@1,long-aa,500,aa
|
||||
tie-greater-last,reply,m15,room1,,,s1@1,short-z,500,z
|
||||
tie-greater-last,expect_winner,m14,room1,,,s1@1,lower-a,,a
|
||||
tie-greater-last,expect_winner,m15,room1,,,s1@1,short-z,,z
|
||||
one-version-rule,version_ok,notice_version,room1,,,,4294967295,,version_ok-notice_version-4294967295
|
||||
one-version-rule,version_refused,notice_version,room1,,,,4294967296,,version_refused-notice_version-4294967296
|
||||
one-version-rule,version_refused,notice_version,room1,,,,01,,version_refused-notice_version-01
|
||||
one-version-rule,version_ok,sample_id,room1,,,,4294967295,,version_ok-sample_id-4294967295
|
||||
one-version-rule,version_refused,sample_id,room1,,,,4294967296,,version_refused-sample_id-4294967296
|
||||
one-version-rule,version_refused,sample_id,room1,,,,01,,version_refused-sample_id-01
|
||||
one-version-rule,version_ok,terms_version,room1,,,,4294967295,,version_ok-terms_version-4294967295
|
||||
one-version-rule,version_refused,terms_version,room1,,,,4294967296,,version_refused-terms_version-4294967296
|
||||
one-version-rule,version_refused,terms_version,room1,,,,01,,version_refused-terms_version-01
|
||||
|
Loading…
Reference in a new issue