Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba536eec1 | ||
|
|
e119e76301 | ||
|
|
afe05b0c7e | ||
|
|
2467e1b025 | ||
|
|
6ae2da0dad | ||
|
|
86a0f891f2 | ||
|
|
b62eb5d6f4 | ||
|
|
f2ab72d84d | ||
|
|
b13619fa27 | ||
|
|
649844aa59 | ||
|
|
1444e0c4f7 | ||
|
|
c3991ec1e8 | ||
|
|
896a6a1c83 | ||
|
|
852c34b60a | ||
|
|
d8f5e1f2fe | ||
|
|
89e80a3402 | ||
|
|
19f04d230a | ||
|
|
fa7d95191a | ||
|
|
a758787258 | ||
|
|
b7d154bc2f | ||
|
|
9a2a5d0736 | ||
|
|
cbb59574b0 | ||
|
|
dfa81973e6 | ||
|
|
3ee4aaac6c |
49 changed files with 5258 additions and 15 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -25,6 +25,4 @@ flutter/eve_parity/assets/repixelizer/
|
|||
*.deb
|
||||
*.ipa
|
||||
*.dSYM/
|
||||
.voidbot/birth/
|
||||
.voidbot/logs/
|
||||
.voidbot/state/*.msgpack
|
||||
/target
|
||||
|
|
|
|||
603
Cargo.lock
generated
Normal file
603
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,603 @@
|
|||
# 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 = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
||||
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.2.0"
|
||||
source = "git+https://github.com/GameCult/CultLib.git?rev=b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7#b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"cultcache-rs-derive",
|
||||
"fs2",
|
||||
"redb",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cultcache-rs-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/GameCult/CultLib.git?rev=b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7#b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[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 = "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.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[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 = "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 = "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.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb338a6c67830a61bed824b78c2bb034ab1ff401a20616bd7957524f4fdc2f22"
|
||||
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 = "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.8",
|
||||
]
|
||||
|
||||
[[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 = "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 = "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.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thing-contract"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cultcache-rs",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||
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-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"
|
||||
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7", package = "cultcache-rs" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
rmp-serde = "1.3"
|
||||
55
README.md
55
README.md
|
|
@ -1,4 +1,41 @@
|
|||
# Eve
|
||||
# Thing
|
||||
|
||||
This repository is Thing (the Eve display, control and sensor edge for GameCult
|
||||
apps). Its present tree is Thing 1, kept running for live consumers until each
|
||||
ports; Thing 2.0 is built here (`docs/thing2-target.md`). The Thing 1 renderer
|
||||
repos are archived under their Eve names: EveUnity, EveFlutter, EveElectron,
|
||||
EveTui, EvePlugins and EveConformance.
|
||||
|
||||
## Start Here
|
||||
|
||||
Value: services publish a surface once, as a typed retained document
|
||||
(`gamecult.eve.surface.v1`), and every runtime lowers it locally. Providers keep
|
||||
authority over state and commands; Eve owns projection, input capture and
|
||||
timestamped sensor publication.
|
||||
|
||||
Problem: without a shared surface contract, each service hand-builds its own
|
||||
dashboard and each renderer re-implements layout, binding and commands.
|
||||
|
||||
Live (Thing 1):
|
||||
|
||||
- `packages/org.gamecult.eve.surface`: renderer-neutral C# surface contract and
|
||||
composition builders; tests in `tests/GameCult.Eve.Surface.Serialization.Tests`.
|
||||
- `packages/eve-contracts`: TypeScript types and validators generated from
|
||||
`schemas/` (`npm run generate`, `npm test`).
|
||||
- `packages/eve-browser-lowering`: browser DOM/CSS lowering of surface documents
|
||||
(`npm test`).
|
||||
- `web/`: browser reference oracle (`web/index.html`, `web/eve-dsl.js`; tests
|
||||
with `node --test web/eve-dsl.test.mjs`).
|
||||
- `docs/surface-contract-v1.md`: the contract. `docs/repo-strategy.md`: repo
|
||||
ownership. `docs/parity-testing-harness.md` with
|
||||
`scripts/run-parity-harness.ps1`: parity runs.
|
||||
- `android/` and `Sources/` (iOS Theos app, `make package`): native proof
|
||||
clients; build steps under Build Shape below.
|
||||
|
||||
Planned (Thing 2.0, in progress): `crates/thing-contract` is the first Rust
|
||||
piece. The layout solver, text, accessibility tree, closed render-command list
|
||||
and thin backends described in `docs/thing2-target.md` and `docs/thing2-map.md`
|
||||
are not built yet; `docs/thing-campaign.md` holds the rename plan.
|
||||
|
||||
CultMesh-based surface web, portable control layer, and timestamped sensor
|
||||
edge.
|
||||
|
|
@ -209,11 +246,8 @@ render:
|
|||
|
||||
## VoidBot CEF Stream
|
||||
|
||||
Start the Starfire relay from `E:\Projects\VoidBot`:
|
||||
|
||||
```powershell
|
||||
npm run swarm:eve-cef-relay -- --width 1620 --height 2160 --scale 2 --port 8792
|
||||
```
|
||||
The CEF relay that produced these frames is not in VoidBot's default branch, so
|
||||
no relay command is documented here until one exists.
|
||||
|
||||
EveCanvas displays binary JPEG frames and returns touch events only when a
|
||||
stream lowering is configured through `EVE_STREAM_URLS`. The native app no
|
||||
|
|
@ -221,12 +255,11 @@ longer guesses a Starfire relay address.
|
|||
|
||||
## Mimir Sensor Uplink
|
||||
|
||||
Start Mimir with `config/mimir-runtime.raven-eve.example.json` or run the
|
||||
receiver processes directly:
|
||||
Run the receiver processes from a Mimir checkout (a sibling `..\Mimir` here):
|
||||
|
||||
```powershell
|
||||
dotnet run --project E:\Projects\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8793 --path /eve/camera --source-id eve-camera --type video-frame
|
||||
dotnet run --project E:\Projects\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8794 --path /eve/mic --source-id eve-mic --type audio-block
|
||||
dotnet run --project ..\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8793 --path /eve/camera --source-id eve-camera --type video-frame
|
||||
dotnet run --project ..\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8794 --path /eve/mic --source-id eve-mic --type audio-block
|
||||
```
|
||||
|
||||
EveCanvas sends binary `mimir.eve_media_observation.v1` camera and microphone
|
||||
|
|
@ -240,7 +273,7 @@ guess receiver URLs.
|
|||
Start the dashboard authority on Starfire:
|
||||
|
||||
```powershell
|
||||
dotnet run --project E:\Projects\Mimir\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --port 8795
|
||||
dotnet run --project ..\Mimir\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --port 8795
|
||||
```
|
||||
|
||||
EveCanvas connects to the Mimir dashboard WebSocket lowering for native retained
|
||||
|
|
|
|||
15
crates/thing-contract/Cargo.toml
Normal file
15
crates/thing-contract/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "thing-contract"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
cultcache-rs = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_bytes = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
rmp-serde = { workspace = true }
|
||||
39
crates/thing-contract/src/asset.rs
Normal file
39
crates/thing-contract/src/asset.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//! An asset catalog: the images a provider's surfaces reference by catalog id and asset id.
|
||||
|
||||
use cultcache_rs::{CultSlots, DatabaseEntry};
|
||||
|
||||
use crate::domain::{Count, Segment, Version};
|
||||
use crate::keys::{ProviderId, asset_catalog_key};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct AssetEntry {
|
||||
#[cultcache(key = 0)]
|
||||
pub id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub media_type: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub uri: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub width: Count,
|
||||
#[cultcache(key = 4)]
|
||||
pub height: Count,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "gamecult.thing.asset_catalog", schema = "gamecult.thing.asset_catalog.v1")]
|
||||
pub struct AssetCatalog {
|
||||
#[cultcache(key = 0)]
|
||||
pub provider_id: ProviderId,
|
||||
#[cultcache(key = 1)]
|
||||
pub catalog_id: Segment,
|
||||
#[cultcache(key = 2)]
|
||||
pub version: Version,
|
||||
#[cultcache(key = 3)]
|
||||
pub assets: Vec<AssetEntry>,
|
||||
}
|
||||
|
||||
impl AssetCatalog {
|
||||
pub fn record_key(&self) -> String {
|
||||
asset_catalog_key(&self.provider_id, &self.catalog_id)
|
||||
}
|
||||
}
|
||||
123
crates/thing-contract/src/command.rs
Normal file
123
crates/thing-contract/src/command.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
//! Commands: what a surface declares, and the one envelope a consumer sends. The caller's
|
||||
//! identity has no field here: the transport sets it.
|
||||
|
||||
use cultcache_rs::{CultSlots, CultUnion, DatabaseEntry};
|
||||
|
||||
use crate::domain::Version;
|
||||
use crate::keys::{CommandId, NodeId, SchemaId, invocation_key};
|
||||
use crate::source::ParamName;
|
||||
|
||||
/// A command a surface declares. A node may reference only a declared command.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CommandDecl {
|
||||
#[cultcache(key = 0)]
|
||||
pub id: CommandId,
|
||||
#[cultcache(key = 1)]
|
||||
pub payload_schema: SchemaId,
|
||||
/// The input nodes whose current values travel with an invocation.
|
||||
#[cultcache(key = 2)]
|
||||
pub captures: Vec<NodeId>,
|
||||
#[cultcache(key = 3)]
|
||||
pub confirm: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CommandIdRef {
|
||||
#[cultcache(key = 0)]
|
||||
pub id: CommandId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum CommandRef {
|
||||
#[cultcache(key = 0)]
|
||||
Id(CommandIdRef),
|
||||
#[cultcache(key = 1)]
|
||||
Param(ParamName),
|
||||
}
|
||||
|
||||
/// A typed record of the declared `payload_schema`, as its MessagePack bytes.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct RecordPayload {
|
||||
#[cultcache(key = 0)]
|
||||
pub schema: SchemaId,
|
||||
#[cultcache(key = 1, bytes)]
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct TextValue {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct NumberValue {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct BoolValue {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ChoiceValue {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum InputValue {
|
||||
#[cultcache(key = 0)]
|
||||
Text(TextValue),
|
||||
#[cultcache(key = 1)]
|
||||
Number(NumberValue),
|
||||
#[cultcache(key = 2)]
|
||||
Bool(BoolValue),
|
||||
#[cultcache(key = 3)]
|
||||
Choice(ChoiceValue),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CapturedInput {
|
||||
#[cultcache(key = 0)]
|
||||
pub node: NodeId,
|
||||
#[cultcache(key = 1)]
|
||||
pub value: InputValue,
|
||||
}
|
||||
|
||||
/// A consumer's request that a provider run a declared command. Its fields are public, so a
|
||||
/// result's copy of `invocation_id` is a convenience and not provenance: core binds results
|
||||
/// to the invocations it issued.
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "gamecult.thing.command_invocation", schema = "gamecult.thing.command_invocation.v1")]
|
||||
pub struct CommandInvocation {
|
||||
/// A UUID string, unique per invocation.
|
||||
#[cultcache(key = 0)]
|
||||
pub invocation_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub surface_key: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub surface_version: Version,
|
||||
#[cultcache(key = 3)]
|
||||
pub command: CommandId,
|
||||
#[cultcache(key = 4)]
|
||||
pub payload: RecordPayload,
|
||||
#[cultcache(key = 5)]
|
||||
pub captured: Vec<CapturedInput>,
|
||||
#[cultcache(key = 6)]
|
||||
pub client: String,
|
||||
/// RFC 3339, UTC.
|
||||
#[cultcache(key = 7)]
|
||||
pub issued_at: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub delegation: Option<String>,
|
||||
}
|
||||
|
||||
impl CommandInvocation {
|
||||
pub fn record_key(&self) -> String {
|
||||
invocation_key(&self.invocation_id)
|
||||
}
|
||||
}
|
||||
216
crates/thing-contract/src/domain.rs
Normal file
216
crates/thing-contract/src/domain.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
//! The value domains of the contract's numbers and local names. Each domain is one field
|
||||
//! type, and `new` is its only door: constructors, struct literals and every serde
|
||||
//! `Deserializer` reach a value through it, so a value out of domain cannot exist.
|
||||
//!
|
||||
//! Each type writes and reads exactly the leaf of its inner type (`Int32`, `Int64`,
|
||||
//! `String`), so the bytes are the plain leaf's. The reference's unsigned leaves are absent
|
||||
//! from the TypeScript and Python packages, which is why the bound is a checked type here
|
||||
//! and a refusal golden there.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
|
||||
/// A value was outside its domain. Names the domain and its bound, never the value.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct DomainError {
|
||||
domain: &'static str,
|
||||
bound: &'static str,
|
||||
}
|
||||
|
||||
impl DomainError {
|
||||
const fn new(domain: &'static str, bound: &'static str) -> Self {
|
||||
Self { domain, bound }
|
||||
}
|
||||
|
||||
/// The type that refused.
|
||||
pub fn domain(&self) -> &'static str {
|
||||
self.domain
|
||||
}
|
||||
|
||||
/// The bound that was not met.
|
||||
pub fn bound(&self) -> &'static str {
|
||||
self.bound
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DomainError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} must be {}", self.domain, self.bound)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DomainError {}
|
||||
|
||||
/// A result's position in its invocation's answers, counting from 1. `new` is the only door:
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::Seq;
|
||||
/// assert!(Seq::new(1).is_ok());
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0423
|
||||
/// use thing_contract::Seq;
|
||||
/// let _ = Seq(0);
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0616
|
||||
/// use thing_contract::Seq;
|
||||
/// let mut seq = Seq::FIRST;
|
||||
/// seq.0 = 0;
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0599
|
||||
/// use thing_contract::Seq;
|
||||
/// let _ = Seq::default();
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// use thing_contract::Seq;
|
||||
/// let _ = Seq::from(0);
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Seq(i32);
|
||||
|
||||
impl Seq {
|
||||
pub const FIRST: Seq = Seq(1);
|
||||
|
||||
pub fn new(value: i32) -> Result<Self, DomainError> {
|
||||
if value >= 1 { Ok(Self(value)) } else { Err(DomainError::new("Seq", "at least 1")) }
|
||||
}
|
||||
|
||||
pub fn get(self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// The following position; stays at `i32::MAX` rather than leaving the domain.
|
||||
pub fn next(self) -> Seq {
|
||||
Seq(self.0.saturating_add(1))
|
||||
}
|
||||
}
|
||||
|
||||
/// A document or source version, never negative. `new` is the only door:
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::Version;
|
||||
/// assert!(Version::new(0).is_ok());
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0423
|
||||
/// use thing_contract::Version;
|
||||
/// let _ = Version(-1);
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0599
|
||||
/// use thing_contract::Version;
|
||||
/// let _ = Version::default();
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Version(i64);
|
||||
|
||||
impl Version {
|
||||
pub fn new(value: i64) -> Result<Self, DomainError> {
|
||||
if value >= 0 { Ok(Self(value)) } else { Err(DomainError::new("Version", "at least 0")) }
|
||||
}
|
||||
|
||||
pub fn get(self) -> i64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A count, an extent or an index, never negative. `new` is the only door:
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::Count;
|
||||
/// assert!(Count::new(0).is_ok());
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0423
|
||||
/// use thing_contract::Count;
|
||||
/// let _ = Count(-1);
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// use thing_contract::Count;
|
||||
/// let _: Count = (-1).into();
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Count(i32);
|
||||
|
||||
impl Count {
|
||||
pub fn new(value: i32) -> Result<Self, DomainError> {
|
||||
if value >= 0 { Ok(Self(value)) } else { Err(DomainError::new("Count", "at least 0")) }
|
||||
}
|
||||
|
||||
pub fn get(self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A name the contract joins into a record key: non-empty, no `.`. `new` is the only door:
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::Segment;
|
||||
/// assert!(Segment::new("main").is_ok());
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0423
|
||||
/// use thing_contract::Segment;
|
||||
/// let _ = Segment(String::from("a.b"));
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// use thing_contract::Segment;
|
||||
/// let _: Segment = "a.b".into();
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct Segment(String);
|
||||
|
||||
impl Segment {
|
||||
pub fn new(value: impl Into<String>) -> Result<Self, DomainError> {
|
||||
let value = value.into();
|
||||
if value.is_empty() {
|
||||
Err(DomainError::new("Segment", "non-empty"))
|
||||
} else if value.contains('.') {
|
||||
Err(DomainError::new("Segment", "free of '.'"))
|
||||
} else {
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! leaf_serde {
|
||||
($name:ident, $inner:ty, $serialize:ident) => {
|
||||
impl Serialize for $name {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.$serialize(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for $name {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
Self::new(<$inner>::deserialize(deserializer)?).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
leaf_serde!(Seq, i32, serialize_i32);
|
||||
leaf_serde!(Version, i64, serialize_i64);
|
||||
leaf_serde!(Count, i32, serialize_i32);
|
||||
|
||||
impl Serialize for Segment {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Segment {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
Self::new(String::deserialize(deserializer)?).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
70
crates/thing-contract/src/keys.rs
Normal file
70
crates/thing-contract/src/keys.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//! Identifier newtypes and the record key of each kind. No caller spells a key as a free string.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::domain::{Segment, Seq, Version};
|
||||
|
||||
macro_rules! id_type {
|
||||
($(#[$doc:meta])* $name:ident) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl $name {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
id_type!(
|
||||
/// A provider of surfaces and state.
|
||||
ProviderId
|
||||
);
|
||||
id_type!(
|
||||
/// The schema id of a CultCache entry type, for example `gamecult.thing.surface.v1`.
|
||||
SchemaId
|
||||
);
|
||||
id_type!(
|
||||
/// The record key of a state record.
|
||||
RecordKey
|
||||
);
|
||||
id_type!(
|
||||
/// A node id, unique within its document after composition expansion.
|
||||
NodeId
|
||||
);
|
||||
id_type!(
|
||||
/// A command declared by a surface.
|
||||
CommandId
|
||||
);
|
||||
|
||||
// Each key joins free-string provider or publisher ids (dotted, as the Verse names them)
|
||||
// with typed trailing parts: a `Segment` holds no dot and a `Seq` or `Version` is digits. A
|
||||
// key therefore splits at its last dot (surface, asset catalog, result) or last two
|
||||
// (composition), so two different field sets never build one key.
|
||||
|
||||
pub(crate) fn surface_key(provider: &ProviderId, label: &Segment) -> String {
|
||||
format!("thing:surface:{}.{}", provider.as_str(), label.as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn composition_key(publisher: &str, name: &Segment, version: Version) -> String {
|
||||
format!("thing:composition:{publisher}.{}.v{}", name.as_str(), version.get())
|
||||
}
|
||||
|
||||
pub(crate) fn invocation_key(invocation_id: &str) -> String {
|
||||
format!("thing:invocation:{invocation_id}")
|
||||
}
|
||||
|
||||
pub(crate) fn result_key(invocation_id: &str, seq: Seq) -> String {
|
||||
format!("thing:result:{invocation_id}.{}", seq.get())
|
||||
}
|
||||
|
||||
pub(crate) fn asset_catalog_key(provider: &ProviderId, catalog_id: &Segment) -> String {
|
||||
format!("thing:asset_catalog:{}.{}", provider.as_str(), catalog_id.as_str())
|
||||
}
|
||||
49
crates/thing-contract/src/lib.rs
Normal file
49
crates/thing-contract/src/lib.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//! The `gamecult.thing.*` wire types and their bytes. This crate owns the contract: the
|
||||
//! golden payloads it blesses are what every other language package and core are held to.
|
||||
//!
|
||||
//! Every kind is a CultCache `DatabaseEntry`; every closed set is a `CultUnion` written
|
||||
//! `[armKey, [slots...]]`, the shape the C# reference reads and writes.
|
||||
//!
|
||||
//! Decoding is a door as well as the constructors: every kind keeps its public serde
|
||||
//! `Deserialize` (CultCache's `DatabaseEntry` requires it). The domains therefore live in the
|
||||
//! field types (`Seq`, `Version`, `Count`, `Segment`), whose `Deserialize` calls the same
|
||||
//! `new` the constructors call, so no door skips a check.
|
||||
//!
|
||||
//! Hazard: decoding these kinds from untrusted bytes on a default-size thread can abort the
|
||||
//! process inside CultCache's nesting bound before the bound refuses (the stack is not
|
||||
//! bounded by nesting depth). The owner is `cultlib-gaps:follow_up:decode-stack-safety`; the
|
||||
//! tests decode on a 16 MiB thread until it lands.
|
||||
|
||||
mod asset;
|
||||
mod command;
|
||||
mod domain;
|
||||
mod keys;
|
||||
mod node;
|
||||
mod result;
|
||||
mod source;
|
||||
mod style;
|
||||
|
||||
pub use asset::*;
|
||||
pub use command::*;
|
||||
pub use domain::{Count, DomainError, Segment, Seq, Version};
|
||||
pub use keys::{CommandId, NodeId, ProviderId, RecordKey, SchemaId};
|
||||
pub use node::*;
|
||||
pub use result::*;
|
||||
pub use source::*;
|
||||
pub use style::*;
|
||||
|
||||
use cultcache_rs::{CultCache, CultCacheRegistry};
|
||||
|
||||
/// Registers the five kinds with a cache.
|
||||
pub struct ThingContract;
|
||||
|
||||
impl CultCacheRegistry for ThingContract {
|
||||
fn register_entries(&self, cache: &mut CultCache) -> anyhow::Result<()> {
|
||||
cache.register_entry_type::<SurfaceDocument>()?;
|
||||
cache.register_entry_type::<CompositionDocument>()?;
|
||||
cache.register_entry_type::<CommandInvocation>()?;
|
||||
cache.register_entry_type::<CommandResult>()?;
|
||||
cache.register_entry_type::<AssetCatalog>()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
455
crates/thing-contract/src/node.rs
Normal file
455
crates/thing-contract/src/node.rs
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
//! Thing Script: the closed node union, the surface document that holds a tree of nodes, and the
|
||||
//! composition document that names a reusable subtree. Arm keys are fixed for v1.
|
||||
|
||||
use cultcache_rs::{CultSlots, CultUnion, DatabaseEntry};
|
||||
|
||||
use crate::domain::{Count, Segment, Version};
|
||||
use crate::command::{CommandDecl, CommandRef};
|
||||
use crate::keys::{NodeId, ProviderId, composition_key, surface_key};
|
||||
use crate::source::{Bind, BoolSource, CollectionSource, MemberPath, NumberSource, TextSource};
|
||||
use crate::style::{BoxStyle, Paint, Role};
|
||||
|
||||
/// What every node carries.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct NodeCommon {
|
||||
#[cultcache(key = 0)]
|
||||
pub id: NodeId,
|
||||
#[cultcache(key = 1)]
|
||||
pub visible: BoolSource,
|
||||
#[cultcache(key = 2)]
|
||||
pub a11y_label: Option<TextSource>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Node {
|
||||
#[cultcache(key = 0)]
|
||||
Box(BoxNode),
|
||||
#[cultcache(key = 1)]
|
||||
Text(TextNode),
|
||||
#[cultcache(key = 2)]
|
||||
Image(ImageNode),
|
||||
#[cultcache(key = 3)]
|
||||
Button(ButtonNode),
|
||||
#[cultcache(key = 4)]
|
||||
Toggle(ToggleNode),
|
||||
#[cultcache(key = 5)]
|
||||
Slider(SliderNode),
|
||||
#[cultcache(key = 6)]
|
||||
TextInput(TextInputNode),
|
||||
#[cultcache(key = 7)]
|
||||
Select(SelectNode),
|
||||
#[cultcache(key = 8)]
|
||||
Repeat(RepeatNode),
|
||||
#[cultcache(key = 9)]
|
||||
Overlay(OverlayNode),
|
||||
#[cultcache(key = 10)]
|
||||
Plot(PlotNode),
|
||||
#[cultcache(key = 11)]
|
||||
Embed(EmbedNode),
|
||||
#[cultcache(key = 12)]
|
||||
Use(UseNode),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct BoxNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub style: BoxStyle,
|
||||
#[cultcache(key = 2)]
|
||||
pub paint: Paint,
|
||||
#[cultcache(key = 3)]
|
||||
pub role: Role,
|
||||
#[cultcache(key = 4)]
|
||||
pub children: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct TextNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub content: TextSource,
|
||||
#[cultcache(key = 2)]
|
||||
pub style_token: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub wrap: bool,
|
||||
#[cultcache(key = 4)]
|
||||
pub max_lines: Option<Count>,
|
||||
#[cultcache(key = 5)]
|
||||
pub selectable: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum ImageFit {
|
||||
#[cultcache(key = 0)]
|
||||
Contain,
|
||||
#[cultcache(key = 1)]
|
||||
Cover,
|
||||
#[cultcache(key = 2)]
|
||||
Fill,
|
||||
#[cultcache(key = 3)]
|
||||
Unscaled,
|
||||
}
|
||||
|
||||
/// An asset of a catalog, with the alternative text a reader without the picture needs.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ImageNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub catalog: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub asset: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub fit: ImageFit,
|
||||
#[cultcache(key = 4)]
|
||||
pub alt: TextSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ButtonNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub content: Vec<Node>,
|
||||
#[cultcache(key = 2)]
|
||||
pub on_activate: CommandRef,
|
||||
#[cultcache(key = 3)]
|
||||
pub enabled: BoolSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ToggleNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub checked: BoolSource,
|
||||
#[cultcache(key = 2)]
|
||||
pub on_change: CommandRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct SliderNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub min: f64,
|
||||
#[cultcache(key = 2)]
|
||||
pub max: f64,
|
||||
#[cultcache(key = 3)]
|
||||
pub step: f64,
|
||||
#[cultcache(key = 4)]
|
||||
pub value: NumberSource,
|
||||
#[cultcache(key = 5)]
|
||||
pub commit: CommandRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum InputKind {
|
||||
#[cultcache(key = 0)]
|
||||
Text,
|
||||
#[cultcache(key = 1)]
|
||||
Number,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct TextInputNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub multiline: bool,
|
||||
#[cultcache(key = 2)]
|
||||
pub kind: InputKind,
|
||||
#[cultcache(key = 3)]
|
||||
pub placeholder: TextSource,
|
||||
#[cultcache(key = 4)]
|
||||
pub initial: TextSource,
|
||||
#[cultcache(key = 5)]
|
||||
pub submit: CommandRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct SelectOption {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct LiteralOptions {
|
||||
#[cultcache(key = 0)]
|
||||
pub options: Vec<SelectOption>,
|
||||
}
|
||||
|
||||
/// Options read from a collection: each item's value and label are members of the item.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct BoundOptions {
|
||||
#[cultcache(key = 0)]
|
||||
pub collection: CollectionSource,
|
||||
#[cultcache(key = 1)]
|
||||
pub value_member: MemberPath,
|
||||
#[cultcache(key = 2)]
|
||||
pub label_member: MemberPath,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum OptionList {
|
||||
#[cultcache(key = 0)]
|
||||
Literal(LiteralOptions),
|
||||
#[cultcache(key = 1)]
|
||||
Bound(BoundOptions),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct SelectNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub options: OptionList,
|
||||
#[cultcache(key = 2)]
|
||||
pub value: Bind,
|
||||
#[cultcache(key = 3)]
|
||||
pub on_change: CommandRef,
|
||||
}
|
||||
|
||||
/// The template is instantiated once per item of the collection, keyed by `key_member` of the item.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct RepeatNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub collection: CollectionSource,
|
||||
#[cultcache(key = 2)]
|
||||
pub key_member: MemberPath,
|
||||
#[cultcache(key = 3)]
|
||||
pub template: std::boxed::Box<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct AnchorNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub node: NodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Anchor {
|
||||
#[cultcache(key = 0)]
|
||||
Viewport,
|
||||
#[cultcache(key = 1)]
|
||||
Node(AnchorNode),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct OverlayNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub anchor: Anchor,
|
||||
#[cultcache(key = 2)]
|
||||
pub modal: bool,
|
||||
#[cultcache(key = 3)]
|
||||
pub dismiss: CommandRef,
|
||||
#[cultcache(key = 4)]
|
||||
pub children: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum PlotMark {
|
||||
#[cultcache(key = 0)]
|
||||
Line,
|
||||
#[cultcache(key = 1)]
|
||||
Bar,
|
||||
#[cultcache(key = 2)]
|
||||
Area,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct PlotNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
/// A numeric list member of a record.
|
||||
#[cultcache(key = 1)]
|
||||
pub series: Bind,
|
||||
#[cultcache(key = 2)]
|
||||
pub mark: PlotMark,
|
||||
#[cultcache(key = 3)]
|
||||
pub x_min: Option<f64>,
|
||||
#[cultcache(key = 4)]
|
||||
pub x_max: Option<f64>,
|
||||
#[cultcache(key = 5)]
|
||||
pub y_min: Option<f64>,
|
||||
#[cultcache(key = 6)]
|
||||
pub y_max: Option<f64>,
|
||||
}
|
||||
|
||||
/// A leaf the host draws: `slot_kind` is a name the host declares it supports.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct EmbedNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub slot_kind: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub slot_id: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub width: f64,
|
||||
#[cultcache(key = 4)]
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CompositionRef {
|
||||
#[cultcache(key = 0)]
|
||||
pub publisher: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub name: Segment,
|
||||
#[cultcache(key = 2)]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct UseNode {
|
||||
#[cultcache(key = 0)]
|
||||
pub common: NodeCommon,
|
||||
#[cultcache(key = 1)]
|
||||
pub composition: CompositionRef,
|
||||
#[cultcache(key = 2)]
|
||||
pub args: Vec<Arg>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Arg {
|
||||
#[cultcache(key = 0)]
|
||||
pub name: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub value: ArgValue,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct TextArg {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct NumberArg {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct BoolArg {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CollectionArg {
|
||||
#[cultcache(key = 0)]
|
||||
pub source: CollectionSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CommandArg {
|
||||
#[cultcache(key = 0)]
|
||||
pub command: CommandRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct SlotArg {
|
||||
#[cultcache(key = 0)]
|
||||
pub nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum ArgValue {
|
||||
#[cultcache(key = 0)]
|
||||
Text(TextArg),
|
||||
#[cultcache(key = 1)]
|
||||
Number(NumberArg),
|
||||
#[cultcache(key = 2)]
|
||||
Bool(BoolArg),
|
||||
#[cultcache(key = 3)]
|
||||
Bind(Bind),
|
||||
#[cultcache(key = 4)]
|
||||
Collection(CollectionArg),
|
||||
#[cultcache(key = 5)]
|
||||
Command(CommandArg),
|
||||
#[cultcache(key = 6)]
|
||||
Slot(SlotArg),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum ParamKind {
|
||||
#[cultcache(key = 0)]
|
||||
Text,
|
||||
#[cultcache(key = 1)]
|
||||
Number,
|
||||
#[cultcache(key = 2)]
|
||||
Bool,
|
||||
#[cultcache(key = 3)]
|
||||
Bind,
|
||||
#[cultcache(key = 4)]
|
||||
Collection,
|
||||
#[cultcache(key = 5)]
|
||||
Command,
|
||||
#[cultcache(key = 6)]
|
||||
Slot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ParamDecl {
|
||||
#[cultcache(key = 0)]
|
||||
pub name: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub kind: ParamKind,
|
||||
}
|
||||
|
||||
/// A provider's surface: a tree of nodes and the commands it declares. A change of structure
|
||||
/// republishes the record with a higher `version`.
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "gamecult.thing.surface", schema = "gamecult.thing.surface.v1")]
|
||||
pub struct SurfaceDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub provider_id: ProviderId,
|
||||
#[cultcache(key = 1)]
|
||||
pub label: Segment,
|
||||
#[cultcache(key = 2)]
|
||||
pub version: Version,
|
||||
#[cultcache(key = 3)]
|
||||
pub title: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub root: Node,
|
||||
#[cultcache(key = 5)]
|
||||
pub commands: Vec<CommandDecl>,
|
||||
}
|
||||
|
||||
impl SurfaceDocument {
|
||||
pub fn record_key(&self) -> String {
|
||||
surface_key(&self.provider_id, &self.label)
|
||||
}
|
||||
}
|
||||
|
||||
/// A reusable subtree with typed parameters. Compositions never own commands.
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "gamecult.thing.composition", schema = "gamecult.thing.composition.v1")]
|
||||
pub struct CompositionDocument {
|
||||
#[cultcache(key = 0)]
|
||||
pub publisher: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub name: Segment,
|
||||
#[cultcache(key = 2)]
|
||||
pub version: Version,
|
||||
#[cultcache(key = 3)]
|
||||
pub params: Vec<ParamDecl>,
|
||||
#[cultcache(key = 4)]
|
||||
pub body: Node,
|
||||
}
|
||||
|
||||
impl CompositionDocument {
|
||||
pub fn record_key(&self) -> String {
|
||||
composition_key(&self.publisher, &self.name, self.version)
|
||||
}
|
||||
}
|
||||
230
crates/thing-contract/src/result.rs
Normal file
230
crates/thing-contract/src/result.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
//! The answer to a command invocation. A `CommandResult` is built by its four constructors,
|
||||
//! each of which copies the invocation's id, and is read through accessors. Decoding is the
|
||||
//! other door and holds the same domain: `seq` is a `Seq`, refused below 1 whichever door
|
||||
//! it enters by. No API takes a state as a string or converts a map.
|
||||
|
||||
use cultcache_rs::{CultSlots, CultUnion, DatabaseEntry};
|
||||
|
||||
use crate::command::{CommandInvocation, CommandRef};
|
||||
use crate::domain::{Seq, Version};
|
||||
use crate::keys::{NodeId, result_key};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct PendingOutcome {
|
||||
#[cultcache(key = 0)]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct CompletedOutcome {
|
||||
#[cultcache(key = 0)]
|
||||
pub source_version: Option<Version>,
|
||||
/// Whether the captured drafts of the invocation clear.
|
||||
#[cultcache(key = 1)]
|
||||
pub clear_drafts: bool,
|
||||
#[cultcache(key = 2)]
|
||||
pub effect: Option<Effect>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct DeniedOutcome {
|
||||
#[cultcache(key = 0)]
|
||||
pub reason: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub node: Option<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct FailedOutcome {
|
||||
#[cultcache(key = 0)]
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Outcome {
|
||||
#[cultcache(key = 0)]
|
||||
Pending(PendingOutcome),
|
||||
#[cultcache(key = 1)]
|
||||
Completed(CompletedOutcome),
|
||||
#[cultcache(key = 2)]
|
||||
Denied(DeniedOutcome),
|
||||
#[cultcache(key = 3)]
|
||||
Failed(FailedOutcome),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct OpenSurfaceEffect {
|
||||
#[cultcache(key = 0)]
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct OpenExternalEffect {
|
||||
#[cultcache(key = 0)]
|
||||
pub url: String,
|
||||
/// The command run when the external step returns.
|
||||
#[cultcache(key = 1)]
|
||||
pub resume: CommandRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Effect {
|
||||
#[cultcache(key = 0)]
|
||||
OpenSurface(OpenSurfaceEffect),
|
||||
#[cultcache(key = 1)]
|
||||
OpenExternal(OpenExternalEffect),
|
||||
}
|
||||
|
||||
/// A provider's answer to one invocation. `seq` counts from 1 per invocation. The copied
|
||||
/// invocation id is a convenience: core binds a result to an invocation it issued.
|
||||
///
|
||||
/// The positive case, a result built by a constructor from the invocation it answers:
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, Seq};
|
||||
/// fn answer(invocation: &CommandInvocation) -> CommandResult {
|
||||
/// CommandResult::failed(invocation, Seq::FIRST, "refused")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A struct literal does not compile, because the fields are private:
|
||||
///
|
||||
/// ```compile_fail,E0451
|
||||
/// use thing_contract::{CommandResult, FailedOutcome, Outcome, Seq};
|
||||
/// let _ = CommandResult {
|
||||
/// invocation_id: String::new(),
|
||||
/// seq: Seq::FIRST,
|
||||
/// outcome: Outcome::Failed(FailedOutcome { reason: String::new() }),
|
||||
/// };
|
||||
/// ```
|
||||
///
|
||||
/// Each field is read through its accessor:
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, Seq};
|
||||
/// fn read(invocation: &CommandInvocation) {
|
||||
/// let result = CommandResult::failed(invocation, Seq::FIRST, "refused");
|
||||
/// let _ = result.invocation_id();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// and assigning it does not compile:
|
||||
///
|
||||
/// ```compile_fail,E0616
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, Seq};
|
||||
/// fn tamper(invocation: &CommandInvocation) {
|
||||
/// let mut result = CommandResult::failed(invocation, Seq::FIRST, "refused");
|
||||
/// result.invocation_id = String::new();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, Seq};
|
||||
/// fn read(invocation: &CommandInvocation) {
|
||||
/// let result = CommandResult::failed(invocation, Seq::FIRST, "refused");
|
||||
/// let _ = result.seq();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0616
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, Seq};
|
||||
/// fn tamper(invocation: &CommandInvocation) {
|
||||
/// let mut result = CommandResult::failed(invocation, Seq::FIRST, "refused");
|
||||
/// result.seq = Seq::FIRST.next();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, Seq};
|
||||
/// fn read(invocation: &CommandInvocation) {
|
||||
/// let result = CommandResult::failed(invocation, Seq::FIRST, "refused");
|
||||
/// let _ = result.outcome();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0616
|
||||
/// use thing_contract::{CommandInvocation, CommandResult, FailedOutcome, Outcome, Seq};
|
||||
/// fn tamper(invocation: &CommandInvocation) {
|
||||
/// let mut result = CommandResult::failed(invocation, Seq::FIRST, "refused");
|
||||
/// result.outcome = Outcome::Failed(FailedOutcome { reason: String::new() });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Nothing converts a map into a result:
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// use std::collections::BTreeMap;
|
||||
/// use thing_contract::CommandResult;
|
||||
/// let flat: BTreeMap<String, String> = BTreeMap::new();
|
||||
/// let _ = CommandResult::from(flat);
|
||||
/// ```
|
||||
///
|
||||
/// Nor a string state:
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// use thing_contract::CommandResult;
|
||||
/// let _ = CommandResult::from("completed");
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
|
||||
#[cultcache(type = "gamecult.thing.command_result", schema = "gamecult.thing.command_result.v1")]
|
||||
pub struct CommandResult {
|
||||
#[cultcache(key = 0)]
|
||||
invocation_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
seq: Seq,
|
||||
#[cultcache(key = 2)]
|
||||
outcome: Outcome,
|
||||
}
|
||||
|
||||
impl CommandResult {
|
||||
fn answering(invocation: &CommandInvocation, seq: Seq, outcome: Outcome) -> Self {
|
||||
Self { invocation_id: invocation.invocation_id.clone(), seq, outcome }
|
||||
}
|
||||
|
||||
pub fn pending(invocation: &CommandInvocation, seq: Seq, message: impl Into<String>) -> Self {
|
||||
Self::answering(invocation, seq, Outcome::Pending(PendingOutcome { message: message.into() }))
|
||||
}
|
||||
|
||||
pub fn completed(
|
||||
invocation: &CommandInvocation,
|
||||
seq: Seq,
|
||||
source_version: Option<Version>,
|
||||
clear_drafts: bool,
|
||||
effect: Option<Effect>,
|
||||
) -> Self {
|
||||
Self::answering(
|
||||
invocation,
|
||||
seq,
|
||||
Outcome::Completed(CompletedOutcome { source_version, clear_drafts, effect }),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn denied(
|
||||
invocation: &CommandInvocation,
|
||||
seq: Seq,
|
||||
reason: impl Into<String>,
|
||||
node: Option<NodeId>,
|
||||
) -> Self {
|
||||
Self::answering(invocation, seq, Outcome::Denied(DeniedOutcome { reason: reason.into(), node }))
|
||||
}
|
||||
|
||||
pub fn failed(invocation: &CommandInvocation, seq: Seq, reason: impl Into<String>) -> Self {
|
||||
Self::answering(invocation, seq, Outcome::Failed(FailedOutcome { reason: reason.into() }))
|
||||
}
|
||||
|
||||
pub fn invocation_id(&self) -> &str {
|
||||
&self.invocation_id
|
||||
}
|
||||
|
||||
pub fn seq(&self) -> Seq {
|
||||
self.seq
|
||||
}
|
||||
|
||||
pub fn outcome(&self) -> &Outcome {
|
||||
&self.outcome
|
||||
}
|
||||
|
||||
pub fn record_key(&self) -> String {
|
||||
result_key(&self.invocation_id, self.seq)
|
||||
}
|
||||
}
|
||||
170
crates/thing-contract/src/source.rs
Normal file
170
crates/thing-contract/src/source.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
//! Where a value comes from: a literal, a binding to a state record, or a composition parameter.
|
||||
//! A member path is a list of names and indices, never an expression.
|
||||
|
||||
use cultcache_rs::{CultSlots, CultUnion};
|
||||
|
||||
use crate::domain::Count;
|
||||
use crate::keys::{ProviderId, RecordKey, SchemaId};
|
||||
|
||||
/// A composition parameter, named.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ParamName {
|
||||
#[cultcache(key = 0)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct TextLiteral {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct NumberLiteral {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct BoolLiteral {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum TextSource {
|
||||
#[cultcache(key = 0)]
|
||||
Literal(TextLiteral),
|
||||
#[cultcache(key = 1)]
|
||||
Bound(Bind),
|
||||
#[cultcache(key = 2)]
|
||||
Param(ParamName),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum NumberSource {
|
||||
#[cultcache(key = 0)]
|
||||
Literal(NumberLiteral),
|
||||
#[cultcache(key = 1)]
|
||||
Bound(Bind),
|
||||
#[cultcache(key = 2)]
|
||||
Param(ParamName),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum BoolSource {
|
||||
#[cultcache(key = 0)]
|
||||
Literal(BoolLiteral),
|
||||
#[cultcache(key = 1)]
|
||||
Bound(Bind),
|
||||
#[cultcache(key = 2)]
|
||||
Param(ParamName),
|
||||
}
|
||||
|
||||
/// One member of a record, read through a path, optionally formatted.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Bind {
|
||||
#[cultcache(key = 0)]
|
||||
pub source: StateSource,
|
||||
#[cultcache(key = 1)]
|
||||
pub member: MemberPath,
|
||||
#[cultcache(key = 2)]
|
||||
pub format: Option<Format>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum StateSource {
|
||||
#[cultcache(key = 0)]
|
||||
Record(StateRef),
|
||||
/// The current item inside a `Repeat` template.
|
||||
#[cultcache(key = 1)]
|
||||
Item,
|
||||
}
|
||||
|
||||
/// A state record. An absent provider is the publisher of the document.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct StateRef {
|
||||
#[cultcache(key = 0)]
|
||||
pub provider: Option<ProviderId>,
|
||||
#[cultcache(key = 1)]
|
||||
pub schema: SchemaId,
|
||||
#[cultcache(key = 2)]
|
||||
pub key: RecordKey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct MemberPath {
|
||||
#[cultcache(key = 0)]
|
||||
pub steps: Vec<MemberStep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum MemberStep {
|
||||
#[cultcache(key = 0)]
|
||||
Name(MemberName),
|
||||
#[cultcache(key = 1)]
|
||||
Index(MemberIndex),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct MemberName {
|
||||
#[cultcache(key = 0)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct MemberIndex {
|
||||
#[cultcache(key = 0)]
|
||||
pub index: Count,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Format {
|
||||
#[cultcache(key = 0)]
|
||||
Number(NumberFormat),
|
||||
#[cultcache(key = 1)]
|
||||
Duration,
|
||||
#[cultcache(key = 2)]
|
||||
Timestamp,
|
||||
#[cultcache(key = 3)]
|
||||
Affix(AffixFormat),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct NumberFormat {
|
||||
#[cultcache(key = 0)]
|
||||
pub precision: Count,
|
||||
#[cultcache(key = 1)]
|
||||
pub percent: bool,
|
||||
#[cultcache(key = 2)]
|
||||
pub unit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct AffixFormat {
|
||||
#[cultcache(key = 0)]
|
||||
pub prefix: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
/// The records of one type, or a list member of one record.
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum CollectionSource {
|
||||
#[cultcache(key = 0)]
|
||||
Records(RecordsSource),
|
||||
#[cultcache(key = 1)]
|
||||
Member(Bind),
|
||||
#[cultcache(key = 2)]
|
||||
Param(ParamName),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct RecordsSource {
|
||||
#[cultcache(key = 0)]
|
||||
pub provider: Option<ProviderId>,
|
||||
#[cultcache(key = 1)]
|
||||
pub schema: SchemaId,
|
||||
#[cultcache(key = 2)]
|
||||
pub key_prefix: String,
|
||||
}
|
||||
241
crates/thing-contract/src/style.rs
Normal file
241
crates/thing-contract/src/style.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
//! Box layout and paint as plain data. The crate holds no layout engine: core maps `BoxStyle`
|
||||
//! onto its solver.
|
||||
|
||||
use cultcache_rs::{CultSlots, CultUnion};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Scalar {
|
||||
#[cultcache(key = 0)]
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Dimension {
|
||||
#[cultcache(key = 0)]
|
||||
Px(Scalar),
|
||||
#[cultcache(key = 1)]
|
||||
Percent(Scalar),
|
||||
#[cultcache(key = 2)]
|
||||
Fr(Scalar),
|
||||
#[cultcache(key = 3)]
|
||||
Auto,
|
||||
#[cultcache(key = 4)]
|
||||
Content,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Size2 {
|
||||
#[cultcache(key = 0)]
|
||||
pub width: Dimension,
|
||||
#[cultcache(key = 1)]
|
||||
pub height: Dimension,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Edges {
|
||||
#[cultcache(key = 0)]
|
||||
pub top: Dimension,
|
||||
#[cultcache(key = 1)]
|
||||
pub right: Dimension,
|
||||
#[cultcache(key = 2)]
|
||||
pub bottom: Dimension,
|
||||
#[cultcache(key = 3)]
|
||||
pub left: Dimension,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Display {
|
||||
#[cultcache(key = 0)]
|
||||
Flex,
|
||||
#[cultcache(key = 1)]
|
||||
Grid,
|
||||
#[cultcache(key = 2)]
|
||||
Block,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Direction {
|
||||
#[cultcache(key = 0)]
|
||||
Row,
|
||||
#[cultcache(key = 1)]
|
||||
Column,
|
||||
#[cultcache(key = 2)]
|
||||
RowReverse,
|
||||
#[cultcache(key = 3)]
|
||||
ColumnReverse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Wrap {
|
||||
#[cultcache(key = 0)]
|
||||
NoWrap,
|
||||
#[cultcache(key = 1)]
|
||||
Wrap,
|
||||
#[cultcache(key = 2)]
|
||||
WrapReverse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Position {
|
||||
#[cultcache(key = 0)]
|
||||
Relative,
|
||||
#[cultcache(key = 1)]
|
||||
Absolute,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Overflow {
|
||||
#[cultcache(key = 0)]
|
||||
Visible,
|
||||
#[cultcache(key = 1)]
|
||||
Clip,
|
||||
#[cultcache(key = 2)]
|
||||
Scroll,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum ItemAlign {
|
||||
#[cultcache(key = 0)]
|
||||
Auto,
|
||||
#[cultcache(key = 1)]
|
||||
Start,
|
||||
#[cultcache(key = 2)]
|
||||
End,
|
||||
#[cultcache(key = 3)]
|
||||
Center,
|
||||
#[cultcache(key = 4)]
|
||||
Stretch,
|
||||
#[cultcache(key = 5)]
|
||||
Baseline,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum ContentAlign {
|
||||
#[cultcache(key = 0)]
|
||||
Auto,
|
||||
#[cultcache(key = 1)]
|
||||
Start,
|
||||
#[cultcache(key = 2)]
|
||||
End,
|
||||
#[cultcache(key = 3)]
|
||||
Center,
|
||||
#[cultcache(key = 4)]
|
||||
Stretch,
|
||||
#[cultcache(key = 5)]
|
||||
SpaceBetween,
|
||||
#[cultcache(key = 6)]
|
||||
SpaceAround,
|
||||
#[cultcache(key = 7)]
|
||||
SpaceEvenly,
|
||||
}
|
||||
|
||||
/// The layout of a box. `gap` is the column gap in `width` and the row gap in `height`.
|
||||
/// `grid_columns` and `grid_rows` are the grid tracks and are read only when `display` is Grid.
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct BoxStyle {
|
||||
#[cultcache(key = 0)]
|
||||
pub display: Display,
|
||||
#[cultcache(key = 1)]
|
||||
pub direction: Direction,
|
||||
#[cultcache(key = 2)]
|
||||
pub wrap: Wrap,
|
||||
#[cultcache(key = 3)]
|
||||
pub gap: Size2,
|
||||
#[cultcache(key = 4)]
|
||||
pub padding: Edges,
|
||||
#[cultcache(key = 5)]
|
||||
pub margin: Edges,
|
||||
#[cultcache(key = 6)]
|
||||
pub size: Size2,
|
||||
#[cultcache(key = 7)]
|
||||
pub min_size: Size2,
|
||||
#[cultcache(key = 8)]
|
||||
pub max_size: Size2,
|
||||
#[cultcache(key = 9)]
|
||||
pub position: Position,
|
||||
#[cultcache(key = 10)]
|
||||
pub inset: Edges,
|
||||
#[cultcache(key = 11)]
|
||||
pub overflow: Overflow,
|
||||
#[cultcache(key = 12)]
|
||||
pub align_items: ItemAlign,
|
||||
#[cultcache(key = 13)]
|
||||
pub align_self: ItemAlign,
|
||||
#[cultcache(key = 14)]
|
||||
pub align_content: ContentAlign,
|
||||
#[cultcache(key = 15)]
|
||||
pub justify_content: ContentAlign,
|
||||
#[cultcache(key = 16)]
|
||||
pub grow: f64,
|
||||
#[cultcache(key = 17)]
|
||||
pub shrink: f64,
|
||||
#[cultcache(key = 18)]
|
||||
pub basis: Dimension,
|
||||
#[cultcache(key = 19)]
|
||||
pub grid_columns: Vec<Dimension>,
|
||||
#[cultcache(key = 20)]
|
||||
pub grid_rows: Vec<Dimension>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct ThemeToken {
|
||||
#[cultcache(key = 0)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Rgba {
|
||||
#[cultcache(key = 0)]
|
||||
pub r: u8,
|
||||
#[cultcache(key = 1)]
|
||||
pub g: u8,
|
||||
#[cultcache(key = 2)]
|
||||
pub b: u8,
|
||||
#[cultcache(key = 3)]
|
||||
pub a: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum ColorRef {
|
||||
#[cultcache(key = 0)]
|
||||
Token(ThemeToken),
|
||||
#[cultcache(key = 1)]
|
||||
Rgba(Rgba),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Border {
|
||||
#[cultcache(key = 0)]
|
||||
pub color: ColorRef,
|
||||
#[cultcache(key = 1)]
|
||||
pub width: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, CultSlots)]
|
||||
pub struct Paint {
|
||||
#[cultcache(key = 0)]
|
||||
pub background: Option<ColorRef>,
|
||||
#[cultcache(key = 1)]
|
||||
pub border: Option<Border>,
|
||||
#[cultcache(key = 2)]
|
||||
pub radius: f64,
|
||||
}
|
||||
|
||||
/// The accessibility role of a box. The accessibility tree itself is core's.
|
||||
#[derive(Clone, Debug, PartialEq, CultUnion)]
|
||||
pub enum Role {
|
||||
#[cultcache(key = 0)]
|
||||
Group,
|
||||
#[cultcache(key = 1)]
|
||||
Region,
|
||||
#[cultcache(key = 2)]
|
||||
List,
|
||||
#[cultcache(key = 3)]
|
||||
ListItem,
|
||||
#[cultcache(key = 4)]
|
||||
Heading,
|
||||
#[cultcache(key = 5)]
|
||||
Dialog,
|
||||
#[cultcache(key = 6)]
|
||||
Toolbar,
|
||||
}
|
||||
146
crates/thing-contract/tests/common/mod.rs
Normal file
146
crates/thing-contract/tests/common/mod.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
//! Constructors for the values the goldens and the other tests share. Every value is built
|
||||
//! through the crate's public types; no bytes are spelled here.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use thing_contract::*;
|
||||
|
||||
pub fn seq(value: i32) -> Seq {
|
||||
Seq::new(value).unwrap()
|
||||
}
|
||||
|
||||
pub fn ver(value: i64) -> Version {
|
||||
Version::new(value).unwrap()
|
||||
}
|
||||
|
||||
pub fn count(value: i32) -> Count {
|
||||
Count::new(value).unwrap()
|
||||
}
|
||||
|
||||
pub fn seg(value: &str) -> Segment {
|
||||
Segment::new(value).unwrap()
|
||||
}
|
||||
|
||||
pub fn nid(id: &str) -> NodeId {
|
||||
NodeId::new(id)
|
||||
}
|
||||
|
||||
pub fn lit_text(value: &str) -> TextSource {
|
||||
TextSource::Literal(TextLiteral { value: value.into() })
|
||||
}
|
||||
|
||||
pub fn lit_bool(value: bool) -> BoolSource {
|
||||
BoolSource::Literal(BoolLiteral { value })
|
||||
}
|
||||
|
||||
pub fn lit_number(value: f64) -> NumberSource {
|
||||
NumberSource::Literal(NumberLiteral { value })
|
||||
}
|
||||
|
||||
pub fn param(name: &str) -> ParamName {
|
||||
ParamName { name: name.into() }
|
||||
}
|
||||
|
||||
pub fn path(names: &[&str]) -> MemberPath {
|
||||
MemberPath { steps: names.iter().map(|name| MemberStep::Name(MemberName { name: (*name).into() })).collect() }
|
||||
}
|
||||
|
||||
pub fn record_bind(member: MemberPath, format: Option<Format>) -> Bind {
|
||||
Bind {
|
||||
source: StateSource::Record(StateRef {
|
||||
provider: Some(ProviderId::new("provider.a")),
|
||||
schema: SchemaId::new("gamecult.thing.test.v1"),
|
||||
key: RecordKey::new("row-1"),
|
||||
}),
|
||||
member,
|
||||
format,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_bind(member: MemberPath) -> Bind {
|
||||
Bind { source: StateSource::Item, member, format: None }
|
||||
}
|
||||
|
||||
pub fn command(id: &str) -> CommandRef {
|
||||
CommandRef::Id(CommandIdRef { id: CommandId::new(id) })
|
||||
}
|
||||
|
||||
pub fn common(id: &str) -> NodeCommon {
|
||||
NodeCommon { id: nid(id), visible: lit_bool(true), a11y_label: None }
|
||||
}
|
||||
|
||||
pub fn px(value: f64) -> Dimension {
|
||||
Dimension::Px(Scalar { value })
|
||||
}
|
||||
|
||||
pub fn size2(width: Dimension, height: Dimension) -> Size2 {
|
||||
Size2 { width, height }
|
||||
}
|
||||
|
||||
pub fn edges(all: Dimension) -> Edges {
|
||||
Edges { top: all.clone(), right: all.clone(), bottom: all.clone(), left: all }
|
||||
}
|
||||
|
||||
pub fn style() -> BoxStyle {
|
||||
BoxStyle {
|
||||
display: Display::Flex,
|
||||
direction: Direction::Column,
|
||||
wrap: Wrap::NoWrap,
|
||||
gap: size2(px(4.0), px(8.0)),
|
||||
padding: edges(px(2.0)),
|
||||
margin: edges(Dimension::Auto),
|
||||
size: size2(Dimension::Percent(Scalar { value: 100.0 }), Dimension::Content),
|
||||
min_size: size2(px(0.0), px(0.0)),
|
||||
max_size: size2(Dimension::Auto, Dimension::Auto),
|
||||
position: Position::Relative,
|
||||
inset: edges(Dimension::Auto),
|
||||
overflow: Overflow::Visible,
|
||||
align_items: ItemAlign::Stretch,
|
||||
align_self: ItemAlign::Auto,
|
||||
align_content: ContentAlign::Start,
|
||||
justify_content: ContentAlign::SpaceBetween,
|
||||
grow: 1.0,
|
||||
shrink: 0.0,
|
||||
basis: Dimension::Auto,
|
||||
grid_columns: vec![],
|
||||
grid_rows: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plain_paint() -> Paint {
|
||||
Paint { background: None, border: None, radius: 0.0 }
|
||||
}
|
||||
|
||||
pub fn boxed(id: &str, children: Vec<Node>) -> Node {
|
||||
Node::Box(BoxNode { common: common(id), style: style(), paint: plain_paint(), role: Role::Group, children })
|
||||
}
|
||||
|
||||
pub fn text(id: &str, content: &str) -> Node {
|
||||
Node::Text(TextNode {
|
||||
common: common(id),
|
||||
content: lit_text(content),
|
||||
style_token: "body".into(),
|
||||
wrap: true,
|
||||
max_lines: None,
|
||||
selectable: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invocation() -> CommandInvocation {
|
||||
CommandInvocation {
|
||||
invocation_id: "3f2b8c1e-5d44-4a6b-9a07-1c2d3e4f5a6b".into(),
|
||||
surface_key: "thing:surface:provider.a.main".into(),
|
||||
surface_version: ver(7),
|
||||
command: CommandId::new("go"),
|
||||
payload: RecordPayload { schema: SchemaId::new("gamecult.thing.test.go.v1"), bytes: vec![0x91, 0x01, 0xff, 0x00] },
|
||||
captured: vec![
|
||||
CapturedInput { node: nid("name-input"), value: InputValue::Text(TextValue { value: "Ada".into() }) },
|
||||
CapturedInput { node: nid("level"), value: InputValue::Number(NumberValue { value: 0.75 }) },
|
||||
CapturedInput { node: nid("flag"), value: InputValue::Bool(BoolValue { value: true }) },
|
||||
CapturedInput { node: nid("pick"), value: InputValue::Choice(ChoiceValue { value: "a".into() }) },
|
||||
],
|
||||
client: "renderer.web".into(),
|
||||
issued_at: "2026-01-01T00:00:00Z".into(),
|
||||
delegation: Some("delegate-1".into()),
|
||||
}
|
||||
}
|
||||
712
crates/thing-contract/tests/goldens.rs
Normal file
712
crates/thing-contract/tests/goldens.rs
Normal file
|
|
@ -0,0 +1,712 @@
|
|||
//! The goldens: payload bytes written by this crate and nobody else, then compared byte for
|
||||
//! byte. The TypeScript and C# packages and core are held to these files; none of them writes
|
||||
//! one. `THING_BLESS=1` rewrites them; any other run compares.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use common::*;
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope, CultCacheRegistry, DatabaseEntry, SingleFileMessagePackBackingStore};
|
||||
use thing_contract::*;
|
||||
|
||||
const PINNED_CLOCK: &str = "2026-01-01T00:00:00Z";
|
||||
// A value that must never appear in a refusal.
|
||||
const CANARY: &str = "CANARY-7f3a91c2";
|
||||
|
||||
fn goldens_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/goldens")
|
||||
}
|
||||
|
||||
fn blessing() -> bool {
|
||||
std::env::var("THING_BLESS").as_deref() == Ok("1")
|
||||
}
|
||||
|
||||
/// Bless the file or compare against it. Returns the committed bytes.
|
||||
fn golden_bytes(file: &str, produced: &[u8]) -> Vec<u8> {
|
||||
let path = goldens_dir().join(file);
|
||||
if blessing() {
|
||||
std::fs::write(&path, produced).unwrap_or_else(|error| panic!("{file}: {error}"));
|
||||
}
|
||||
let committed = std::fs::read(&path).unwrap_or_else(|error| panic!("{file}: {error}"));
|
||||
assert_eq!(committed, produced, "{file}: the committed golden differs from what the crate writes");
|
||||
committed
|
||||
}
|
||||
|
||||
fn golden<T: DatabaseEntry + PartialEq + Debug>(name: &str, value: &T) -> String {
|
||||
let produced = rmp_serde::to_vec(value).unwrap();
|
||||
let committed = golden_bytes(&format!("{name}.bin"), &produced);
|
||||
let decoded: T = rmp_serde::from_slice(&committed).unwrap_or_else(|error| panic!("{name}: {error}"));
|
||||
assert_eq!(&decoded, value, "{name}: the golden does not decode to the constructed value");
|
||||
assert_eq!(rmp_serde::to_vec(&decoded).unwrap(), committed, "{name}: re-encoding changed the bytes");
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
fn bind_all_formats() -> Vec<Bind> {
|
||||
let number = Format::Number(NumberFormat { precision: count(2), percent: true, unit: Some("ms".into()) });
|
||||
let affix = Format::Affix(AffixFormat { prefix: "$".into(), suffix: "/s".into() });
|
||||
let indexed = MemberPath {
|
||||
steps: vec![
|
||||
MemberStep::Name(MemberName { name: "items".into() }),
|
||||
MemberStep::Index(MemberIndex { index: count(3) }),
|
||||
],
|
||||
};
|
||||
vec![
|
||||
record_bind(indexed, Some(number)),
|
||||
record_bind(path(&["elapsed"]), Some(Format::Duration)),
|
||||
record_bind(path(&["at"]), Some(Format::Timestamp)),
|
||||
record_bind(path(&["price"]), Some(affix)),
|
||||
record_bind(path(&["plain"]), None),
|
||||
item_bind(path(&["name"])),
|
||||
]
|
||||
}
|
||||
|
||||
/// A surface using all 13 node arms, nested more than four deep, with every source arm.
|
||||
fn full_surface() -> SurfaceDocument {
|
||||
let binds = bind_all_formats();
|
||||
let bound_text = TextSource::Bound(binds[3].clone());
|
||||
let bound_bool = BoolSource::Bound(binds[4].clone());
|
||||
let bound_number = NumberSource::Bound(binds[0].clone());
|
||||
|
||||
let mut painted = BoxNode {
|
||||
common: NodeCommon {
|
||||
id: nid("painted"),
|
||||
visible: BoolSource::Param(param("show")),
|
||||
a11y_label: Some(TextSource::Param(param("label"))),
|
||||
},
|
||||
style: style(),
|
||||
paint: Paint {
|
||||
background: Some(ColorRef::Token(ThemeToken { name: "surface.raised".into() })),
|
||||
border: Some(Border { color: ColorRef::Rgba(Rgba { r: 1, g: 2, b: 3, a: 255 }), width: 1.5 }),
|
||||
radius: 4.0,
|
||||
},
|
||||
role: Role::Region,
|
||||
children: vec![],
|
||||
};
|
||||
painted.style.display = Display::Grid;
|
||||
painted.style.grid_columns = vec![Dimension::Fr(Scalar { value: 1.0 }), px(120.0)];
|
||||
painted.style.grid_rows = vec![Dimension::Content];
|
||||
painted.style.position = Position::Absolute;
|
||||
painted.style.overflow = Overflow::Scroll;
|
||||
painted.style.direction = Direction::RowReverse;
|
||||
painted.style.wrap = Wrap::WrapReverse;
|
||||
painted.style.align_items = ItemAlign::Baseline;
|
||||
painted.style.align_self = ItemAlign::Center;
|
||||
painted.style.align_content = ContentAlign::SpaceAround;
|
||||
painted.style.justify_content = ContentAlign::SpaceEvenly;
|
||||
painted.children = vec![text("deep-text", "deep")];
|
||||
|
||||
let repeat = Node::Repeat(RepeatNode {
|
||||
common: common("rows"),
|
||||
collection: CollectionSource::Records(RecordsSource {
|
||||
provider: None,
|
||||
schema: SchemaId::new("gamecult.thing.test.v1"),
|
||||
key_prefix: "row-".into(),
|
||||
}),
|
||||
key_member: path(&["id"]),
|
||||
template: std::boxed::Box::new(Node::Text(TextNode {
|
||||
common: common("row-name"),
|
||||
content: TextSource::Bound(item_bind(path(&["name"]))),
|
||||
style_token: "body".into(),
|
||||
wrap: false,
|
||||
max_lines: Some(count(2)),
|
||||
selectable: true,
|
||||
})),
|
||||
});
|
||||
|
||||
let leaves = vec![
|
||||
Node::Text(TextNode {
|
||||
common: common("t-bound"),
|
||||
content: bound_text,
|
||||
style_token: "label".into(),
|
||||
wrap: true,
|
||||
max_lines: None,
|
||||
selectable: false,
|
||||
}),
|
||||
Node::Image(ImageNode {
|
||||
common: common("img"),
|
||||
catalog: "icons".into(),
|
||||
asset: "gear".into(),
|
||||
fit: ImageFit::Cover,
|
||||
alt: lit_text("a gear"),
|
||||
}),
|
||||
Node::Button(ButtonNode {
|
||||
common: common("go"),
|
||||
content: vec![text("go-label", "Go")],
|
||||
on_activate: command("go"),
|
||||
enabled: bound_bool.clone(),
|
||||
}),
|
||||
Node::Toggle(ToggleNode { common: common("flag"), checked: bound_bool, on_change: CommandRef::Param(param("toggle")) }),
|
||||
Node::Slider(SliderNode {
|
||||
common: common("level"),
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
step: 0.25,
|
||||
value: bound_number,
|
||||
commit: command("set-level"),
|
||||
}),
|
||||
Node::TextInput(TextInputNode {
|
||||
common: common("name-input"),
|
||||
multiline: true,
|
||||
kind: InputKind::Number,
|
||||
placeholder: lit_text("name"),
|
||||
initial: TextSource::Param(param("initial")),
|
||||
submit: command("rename"),
|
||||
}),
|
||||
Node::TextInput(TextInputNode {
|
||||
common: common("title-input"),
|
||||
multiline: false,
|
||||
kind: InputKind::Text,
|
||||
placeholder: lit_text("title"),
|
||||
initial: TextSource::Param(param("initial")),
|
||||
submit: command("retitle"),
|
||||
}),
|
||||
Node::Select(SelectNode {
|
||||
common: common("pick"),
|
||||
options: OptionList::Literal(LiteralOptions {
|
||||
options: vec![SelectOption { value: "a".into(), label: "Alpha".into() }],
|
||||
}),
|
||||
value: binds[4].clone(),
|
||||
on_change: command("pick"),
|
||||
}),
|
||||
Node::Select(SelectNode {
|
||||
common: common("pick-bound"),
|
||||
options: OptionList::Bound(BoundOptions {
|
||||
collection: CollectionSource::Member(binds[0].clone()),
|
||||
value_member: path(&["id"]),
|
||||
label_member: path(&["name"]),
|
||||
}),
|
||||
value: binds[5].clone(),
|
||||
on_change: command("pick"),
|
||||
}),
|
||||
repeat,
|
||||
Node::Overlay(OverlayNode {
|
||||
common: common("menu"),
|
||||
anchor: Anchor::Node(AnchorNode { node: nid("go") }),
|
||||
modal: false,
|
||||
dismiss: command("close"),
|
||||
children: vec![text("menu-item", "one")],
|
||||
}),
|
||||
Node::Overlay(OverlayNode {
|
||||
common: common("dialog"),
|
||||
anchor: Anchor::Viewport,
|
||||
modal: true,
|
||||
dismiss: command("close"),
|
||||
children: vec![],
|
||||
}),
|
||||
Node::Plot(PlotNode {
|
||||
common: common("trend"),
|
||||
series: binds[1].clone(),
|
||||
mark: PlotMark::Area,
|
||||
x_min: Some(0.0),
|
||||
x_max: None,
|
||||
y_min: None,
|
||||
y_max: Some(100.0),
|
||||
}),
|
||||
Node::Embed(EmbedNode {
|
||||
common: common("viewport"),
|
||||
slot_kind: "world.scene".into(),
|
||||
slot_id: "main".into(),
|
||||
width: 640.0,
|
||||
height: 480.0,
|
||||
}),
|
||||
Node::Use(UseNode {
|
||||
common: common("kv"),
|
||||
composition: CompositionRef { publisher: "thing.std".into(), name: seg("key-value"), version: ver(1) },
|
||||
args: vec![Arg { name: "key".into(), value: ArgValue::Text(TextArg { value: "k".into() }) }],
|
||||
}),
|
||||
];
|
||||
|
||||
let mut deep = boxed("d4", vec![Node::Box(painted)]);
|
||||
for id in ["d3", "d2", "d1"] {
|
||||
deep = boxed(id, vec![deep]);
|
||||
}
|
||||
let mut children = vec![deep];
|
||||
children.extend(leaves);
|
||||
for plot_choice in [
|
||||
Node::Plot(PlotNode {
|
||||
common: common("trend-line"),
|
||||
series: binds[2].clone(),
|
||||
mark: PlotMark::Line,
|
||||
x_min: None,
|
||||
x_max: None,
|
||||
y_min: None,
|
||||
y_max: None,
|
||||
}),
|
||||
Node::Plot(PlotNode {
|
||||
common: common("trend-bar"),
|
||||
series: binds[2].clone(),
|
||||
mark: PlotMark::Bar,
|
||||
x_min: None,
|
||||
x_max: None,
|
||||
y_min: None,
|
||||
y_max: None,
|
||||
}),
|
||||
] {
|
||||
children.push(plot_choice);
|
||||
}
|
||||
for (index, fit) in [ImageFit::Contain, ImageFit::Fill, ImageFit::Unscaled].into_iter().enumerate() {
|
||||
children.push(Node::Image(ImageNode {
|
||||
common: common(&format!("img-fit-{index}")),
|
||||
catalog: "icons".into(),
|
||||
asset: "gear".into(),
|
||||
fit,
|
||||
alt: lit_text(""),
|
||||
}));
|
||||
}
|
||||
for (index, role) in [Role::List, Role::ListItem, Role::Heading, Role::Dialog, Role::Toolbar].into_iter().enumerate() {
|
||||
let mut role_style = style();
|
||||
if index == 0 {
|
||||
role_style.direction = Direction::Row;
|
||||
role_style.align_content = ContentAlign::Auto;
|
||||
role_style.justify_content = ContentAlign::Stretch;
|
||||
}
|
||||
children.push(Node::Box(BoxNode {
|
||||
common: common(&format!("role-{index}")),
|
||||
style: role_style,
|
||||
paint: plain_paint(),
|
||||
role,
|
||||
children: vec![],
|
||||
}));
|
||||
}
|
||||
let mut block = BoxNode {
|
||||
common: common("block"),
|
||||
style: style(),
|
||||
paint: plain_paint(),
|
||||
role: Role::Group,
|
||||
children: vec![],
|
||||
};
|
||||
block.style.display = Display::Block;
|
||||
block.style.direction = Direction::ColumnReverse;
|
||||
block.style.wrap = Wrap::Wrap;
|
||||
block.style.overflow = Overflow::Clip;
|
||||
block.style.align_content = ContentAlign::Center;
|
||||
block.style.justify_content = ContentAlign::End;
|
||||
block.style.align_items = ItemAlign::End;
|
||||
block.style.align_self = ItemAlign::Start;
|
||||
block.style.basis = Dimension::Content;
|
||||
children.push(Node::Box(block));
|
||||
children.push(Node::Text(TextNode {
|
||||
common: common("t-param"),
|
||||
content: TextSource::Param(param("title")),
|
||||
style_token: "title".into(),
|
||||
wrap: true,
|
||||
max_lines: None,
|
||||
selectable: false,
|
||||
}));
|
||||
children.push(Node::Toggle(ToggleNode {
|
||||
common: NodeCommon { id: nid("literal-number"), visible: BoolSource::Param(param("v")), a11y_label: None },
|
||||
checked: lit_bool(false),
|
||||
on_change: command("toggle"),
|
||||
}));
|
||||
children.push(Node::Slider(SliderNode {
|
||||
common: common("slider-literal"),
|
||||
min: -1.0,
|
||||
max: 1.0,
|
||||
step: 0.5,
|
||||
value: lit_number(0.5),
|
||||
commit: command("set-level"),
|
||||
}));
|
||||
children.push(Node::Slider(SliderNode {
|
||||
common: common("slider-param"),
|
||||
min: 0.0,
|
||||
max: 10.0,
|
||||
step: 1.0,
|
||||
value: NumberSource::Param(param("n")),
|
||||
commit: command("set-level"),
|
||||
}));
|
||||
children.push(Node::Repeat(RepeatNode {
|
||||
common: common("rows-param"),
|
||||
collection: CollectionSource::Param(param("rows")),
|
||||
key_member: path(&["id"]),
|
||||
template: std::boxed::Box::new(text("row-param", "r")),
|
||||
}));
|
||||
|
||||
SurfaceDocument {
|
||||
provider_id: ProviderId::new("provider.a"),
|
||||
label: seg("main"),
|
||||
version: ver(7),
|
||||
title: "All arms".into(),
|
||||
root: boxed("root", children),
|
||||
commands: vec![
|
||||
CommandDecl {
|
||||
id: CommandId::new("go"),
|
||||
payload_schema: SchemaId::new("gamecult.thing.test.go.v1"),
|
||||
captures: vec![nid("name-input"), nid("level")],
|
||||
confirm: Some("Go now?".into()),
|
||||
},
|
||||
CommandDecl {
|
||||
id: CommandId::new("close"),
|
||||
payload_schema: SchemaId::new("gamecult.thing.test.close.v1"),
|
||||
captures: vec![],
|
||||
confirm: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn full_composition() -> CompositionDocument {
|
||||
let args = vec![
|
||||
Arg { name: "text".into(), value: ArgValue::Text(TextArg { value: "t".into() }) },
|
||||
Arg { name: "number".into(), value: ArgValue::Number(NumberArg { value: 2.5 }) },
|
||||
Arg { name: "bool".into(), value: ArgValue::Bool(BoolArg { value: true }) },
|
||||
Arg { name: "bind".into(), value: ArgValue::Bind(record_bind(path(&["x"]), None)) },
|
||||
Arg {
|
||||
name: "collection".into(),
|
||||
value: ArgValue::Collection(CollectionArg { source: CollectionSource::Param(param("rows")) }),
|
||||
},
|
||||
Arg { name: "command".into(), value: ArgValue::Command(CommandArg { command: command("go") }) },
|
||||
Arg {
|
||||
name: "slot".into(),
|
||||
value: ArgValue::Slot(SlotArg { nodes: vec![text("slot-text", "inside"), boxed("slot-box", vec![])] }),
|
||||
},
|
||||
];
|
||||
let inner_use = Node::Use(UseNode {
|
||||
common: common("inner"),
|
||||
composition: CompositionRef { publisher: "thing.std".into(), name: seg("card"), version: ver(2) },
|
||||
args,
|
||||
});
|
||||
CompositionDocument {
|
||||
publisher: "provider.a".into(),
|
||||
name: seg("panel"),
|
||||
version: ver(3),
|
||||
params: [
|
||||
("title", ParamKind::Text),
|
||||
("count", ParamKind::Number),
|
||||
("on", ParamKind::Bool),
|
||||
("source", ParamKind::Bind),
|
||||
("rows", ParamKind::Collection),
|
||||
("act", ParamKind::Command),
|
||||
("body", ParamKind::Slot),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(name, kind)| ParamDecl { name: name.into(), kind })
|
||||
.collect(),
|
||||
body: boxed("panel-root", vec![text("panel-title", "t"), inner_use]),
|
||||
}
|
||||
}
|
||||
|
||||
fn results(invocation: &CommandInvocation) -> Vec<(&'static str, CommandResult)> {
|
||||
vec![
|
||||
("result-pending", CommandResult::pending(invocation, seq(1), "working")),
|
||||
("result-completed", CommandResult::completed(invocation, seq(2), Some(ver(8)), true, None)),
|
||||
(
|
||||
"result-completed-open-surface",
|
||||
CommandResult::completed(
|
||||
invocation,
|
||||
seq(3),
|
||||
None,
|
||||
false,
|
||||
Some(Effect::OpenSurface(OpenSurfaceEffect { key: "thing:surface:provider.a.next".into() })),
|
||||
),
|
||||
),
|
||||
(
|
||||
"result-completed-open-external",
|
||||
CommandResult::completed(
|
||||
invocation,
|
||||
seq(4),
|
||||
Some(ver(9)),
|
||||
true,
|
||||
Some(Effect::OpenExternal(OpenExternalEffect {
|
||||
url: "https://example.test/auth".into(),
|
||||
resume: command("resume"),
|
||||
})),
|
||||
),
|
||||
),
|
||||
("result-denied", CommandResult::denied(invocation, seq(5), "not yours", Some(nid("go")))),
|
||||
("result-denied-no-node", CommandResult::denied(invocation, seq(6), "not allowed", None)),
|
||||
("result-failed", CommandResult::failed(invocation, seq(7), "broke")),
|
||||
]
|
||||
}
|
||||
|
||||
fn asset_catalog() -> AssetCatalog {
|
||||
AssetCatalog {
|
||||
provider_id: ProviderId::new("provider.a"),
|
||||
catalog_id: seg("icons"),
|
||||
version: ver(2),
|
||||
assets: vec![
|
||||
AssetEntry { id: "gear".into(), media_type: "image/png".into(), uri: "assets/gear.png".into(), width: count(32), height: count(32) },
|
||||
AssetEntry { id: "bolt".into(), media_type: "image/svg+xml".into(), uri: "assets/bolt.svg".into(), width: count(0), height: count(0) },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// A store with one record of each kind, written by cultcache-rs under a pinned clock.
|
||||
fn write_store(path: &std::path::Path) {
|
||||
let _ = std::fs::remove_file(path);
|
||||
let mut cache = CultCache::new();
|
||||
ThingContract.register_entries(&mut cache).unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
|
||||
let surface = full_surface();
|
||||
let composition = full_composition();
|
||||
let invocation = invocation();
|
||||
let result = CommandResult::pending(&invocation, seq(1), "working");
|
||||
let catalog = asset_catalog();
|
||||
put(&mut cache, &surface.record_key(), &surface);
|
||||
put(&mut cache, &composition.record_key(), &composition);
|
||||
put(&mut cache, &invocation.record_key(), &invocation);
|
||||
put(&mut cache, &result.record_key(), &result);
|
||||
put(&mut cache, &catalog.record_key(), &catalog);
|
||||
}
|
||||
|
||||
fn put<T: DatabaseEntry>(cache: &mut CultCache, key: &str, value: &T) {
|
||||
let (mut envelope, _) = cache.prepare_entry(key, value).unwrap();
|
||||
envelope.stored_at = PINNED_CLOCK.into();
|
||||
cache.put_envelope::<T>(envelope).unwrap();
|
||||
}
|
||||
|
||||
fn envelope<T: DatabaseEntry>(key: &str, payload: Vec<u8>) -> CultCacheEnvelope {
|
||||
CultCacheEnvelope {
|
||||
key: key.into(),
|
||||
r#type: T::TYPE.into(),
|
||||
payload,
|
||||
stored_at: PINNED_CLOCK.into(),
|
||||
schema_id: Some(T::TYPE.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_store(path: &std::path::Path) -> CultCache {
|
||||
let mut cache = CultCache::new();
|
||||
ThingContract.register_entries(&mut cache).unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(path)).unwrap();
|
||||
cache.pull_all_backing_stores().unwrap();
|
||||
cache
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goldens_match() {
|
||||
let mut names = BTreeSet::new();
|
||||
names.insert(golden("surface-all-arms", &full_surface()));
|
||||
names.insert(golden("composition-all-params", &full_composition()));
|
||||
let invocation = invocation();
|
||||
names.insert(golden("invocation-all-inputs", &invocation));
|
||||
for (name, result) in results(&invocation) {
|
||||
names.insert(golden(name, &result));
|
||||
}
|
||||
names.insert(golden("asset-catalog", &asset_catalog()));
|
||||
|
||||
// The refusal and tolerance goldens are written by `refusal_payloads` below; a stale file
|
||||
// that no case names is an orphan.
|
||||
let refusals = refusal_payloads();
|
||||
for (name, bytes) in &refusals {
|
||||
golden_bytes(&format!("{name}.bin"), bytes);
|
||||
names.insert((*name).to_string());
|
||||
}
|
||||
let on_disk: BTreeSet<String> = std::fs::read_dir(goldens_dir())
|
||||
.unwrap()
|
||||
.filter_map(|entry| entry.unwrap().file_name().into_string().ok())
|
||||
.filter_map(|file| file.strip_suffix(".bin").map(str::to_string))
|
||||
.collect();
|
||||
assert_eq!(on_disk, names, "every .bin golden is a named case and every case has its file");
|
||||
|
||||
// The store: one record of each kind, written under a pinned clock, byte for byte.
|
||||
let scratch = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("goldens-surface.cc");
|
||||
write_store(&scratch);
|
||||
let committed = golden_bytes("surface.cc", &std::fs::read(&scratch).unwrap());
|
||||
let reopened = goldens_dir().join("surface.cc");
|
||||
assert_eq!(std::fs::read(&reopened).unwrap(), committed);
|
||||
let cache = open_store(&scratch);
|
||||
let surface = full_surface();
|
||||
assert_eq!(cache.get::<SurfaceDocument>(&surface.record_key()).unwrap(), Some(surface));
|
||||
let composition = full_composition();
|
||||
assert_eq!(cache.get::<CompositionDocument>(&composition.record_key()).unwrap(), Some(composition));
|
||||
assert_eq!(cache.get::<CommandInvocation>(&invocation.record_key()).unwrap(), Some(invocation.clone()));
|
||||
let result = CommandResult::pending(&invocation, seq(1), "working");
|
||||
assert_eq!(cache.get::<CommandResult>(&result.record_key()).unwrap(), Some(result));
|
||||
let catalog = asset_catalog();
|
||||
assert_eq!(cache.get::<AssetCatalog>(&catalog.record_key()).unwrap(), Some(catalog));
|
||||
}
|
||||
|
||||
/// The foreign and malformed payloads. Each is the bytes a consumer might send; none is a
|
||||
/// value of this crate, so each is spelled from MessagePack tuples and maps.
|
||||
fn refusal_payloads() -> Vec<(&'static str, Vec<u8>)> {
|
||||
// The map a Thing 1 provider writes for a command result: named members, a receipt
|
||||
// inside, no positional slots.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Receipt {
|
||||
command_id: &'static str,
|
||||
command: &'static str,
|
||||
state: &'static str,
|
||||
message: &'static str,
|
||||
source_version: u64,
|
||||
issued_at_utc: &'static str,
|
||||
}
|
||||
#[derive(serde::Serialize)]
|
||||
struct Flat {
|
||||
receipt: Receipt,
|
||||
}
|
||||
let flat = Flat {
|
||||
receipt: Receipt {
|
||||
command_id: "inv-1",
|
||||
command: "go",
|
||||
state: "completed",
|
||||
message: CANARY,
|
||||
source_version: 4,
|
||||
issued_at_utc: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
};
|
||||
let unit: [u8; 0] = [];
|
||||
let pending = Outcome::Pending(PendingOutcome { message: CANARY.into() });
|
||||
let root = text("root", "x");
|
||||
vec![
|
||||
("result-thing1-flat", rmp_serde::to_vec_named(&flat).unwrap()),
|
||||
// A surface whose root is an arm key the Node union does not declare.
|
||||
(
|
||||
"node-unknown-arm",
|
||||
rmp_serde::to_vec(&("provider.a", "main", 1i64, CANARY, (13i32, (CANARY,)), unit)).unwrap(),
|
||||
),
|
||||
// A result whose outcome is an arm key the Outcome union does not declare.
|
||||
("outcome-unknown-arm", rmp_serde::to_vec(&("inv-1", 1i32, (4i32, (CANARY,)))).unwrap()),
|
||||
// A result with a fourth slot past the last declared one.
|
||||
(
|
||||
"result-extra-slot",
|
||||
rmp_serde::to_vec(&("inv-1", 1i32, (0i32, ("working",)), CANARY)).unwrap(),
|
||||
),
|
||||
// Each domain refusal is a tuple of real values with one slot out of domain.
|
||||
// A result at seq 0.
|
||||
("result-seq-zero", rmp_serde::to_vec(&("inv-1", 0i32, &pending)).unwrap()),
|
||||
// A surface at version -1.
|
||||
(
|
||||
"surface-version-negative",
|
||||
rmp_serde::to_vec(&("provider.a", "main", -1i64, CANARY, &root, unit)).unwrap(),
|
||||
),
|
||||
// A surface whose label holds a dot, so its key would be ambiguous.
|
||||
(
|
||||
"surface-label-dotted",
|
||||
rmp_serde::to_vec(&("provider.a", "b.c", 1i64, CANARY, &root, unit)).unwrap(),
|
||||
),
|
||||
// An asset whose width is negative.
|
||||
(
|
||||
"asset-extent-negative",
|
||||
rmp_serde::to_vec(&("provider.a", "icons", 1i64, [("gear", "image/png", CANARY, -5i32, 32i32)])).unwrap(),
|
||||
),
|
||||
// A colour channel past the u8 leaf.
|
||||
("rgba-channel-out-of-range", rmp_serde::to_vec(&(256i32, 0i32, 0i32, 255i32)).unwrap()),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refusal_cases() {
|
||||
let payloads: std::collections::BTreeMap<_, _> = refusal_payloads().into_iter().collect();
|
||||
let committed = |name: &str| golden_bytes(&format!("{name}.bin"), &payloads[name]);
|
||||
|
||||
// Stored under their key, each is refused naming the kind and the key, never a value.
|
||||
let cases: [(&str, &str, &str); 7] = [
|
||||
("result-thing1-flat", "gamecult.thing.command_result", "thing:result:inv-1.1"),
|
||||
("node-unknown-arm", "gamecult.thing.surface", "thing:surface:provider.a.main"),
|
||||
("outcome-unknown-arm", "gamecult.thing.command_result", "thing:result:inv-1.1"),
|
||||
("result-seq-zero", "gamecult.thing.command_result", "thing:result:inv-1.0"),
|
||||
("surface-version-negative", "gamecult.thing.surface", "thing:surface:provider.a.main"),
|
||||
("surface-label-dotted", "gamecult.thing.surface", "thing:surface:provider.a.b.c"),
|
||||
("asset-extent-negative", "gamecult.thing.asset_catalog", "thing:asset_catalog:provider.a.icons"),
|
||||
];
|
||||
for (name, kind, key) in cases {
|
||||
let mut cache = CultCache::new();
|
||||
ThingContract.register_entries(&mut cache).unwrap();
|
||||
let bytes = committed(name);
|
||||
let error = match kind {
|
||||
"gamecult.thing.surface" => {
|
||||
cache.put_envelope::<SurfaceDocument>(envelope::<SurfaceDocument>(key, bytes)).unwrap_err()
|
||||
}
|
||||
"gamecult.thing.asset_catalog" => {
|
||||
cache.put_envelope::<AssetCatalog>(envelope::<AssetCatalog>(key, bytes)).unwrap_err()
|
||||
}
|
||||
_ => cache.put_envelope::<CommandResult>(envelope::<CommandResult>(key, bytes)).unwrap_err(),
|
||||
};
|
||||
let message = format!("{error:#}");
|
||||
assert!(message.contains(kind), "{name}: the error names the kind: {message}");
|
||||
assert!(message.contains(key), "{name}: the error names the record key: {message}");
|
||||
assert!(!message.contains(CANARY), "{name}: the error echoed a value: {message}");
|
||||
}
|
||||
|
||||
// Decoded directly, the refusal names the union and the arm key, and never the payload.
|
||||
for (name, union, key) in [("node-unknown-arm", "Node", "13"), ("outcome-unknown-arm", "Outcome", "4")] {
|
||||
let bytes = committed(name);
|
||||
let message = if union == "Node" {
|
||||
rmp_serde::from_slice::<SurfaceDocument>(&bytes).unwrap_err().to_string()
|
||||
} else {
|
||||
rmp_serde::from_slice::<CommandResult>(&bytes).unwrap_err().to_string()
|
||||
};
|
||||
assert!(message.contains(union) && message.contains(key), "{name}: {message}");
|
||||
assert!(!message.contains(CANARY), "{name}: {message}");
|
||||
}
|
||||
|
||||
// Decoded directly, a domain refusal names the domain and never the payload.
|
||||
for (name, domain) in [
|
||||
("result-seq-zero", "Seq"),
|
||||
("surface-version-negative", "Version"),
|
||||
("surface-label-dotted", "Segment"),
|
||||
("asset-extent-negative", "Count"),
|
||||
] {
|
||||
let bytes = committed(name);
|
||||
let message = match name {
|
||||
"result-seq-zero" => rmp_serde::from_slice::<CommandResult>(&bytes).unwrap_err().to_string(),
|
||||
"asset-extent-negative" => rmp_serde::from_slice::<AssetCatalog>(&bytes).unwrap_err().to_string(),
|
||||
_ => rmp_serde::from_slice::<SurfaceDocument>(&bytes).unwrap_err().to_string(),
|
||||
};
|
||||
assert!(message.contains(domain), "{name}: {message}");
|
||||
assert!(!message.contains(CANARY), "{name}: {message}");
|
||||
}
|
||||
|
||||
// The u8 leaf refuses a channel past 255 when decoded as an Rgba.
|
||||
let rgba = rmp_serde::from_slice::<Rgba>(&committed("rgba-channel-out-of-range")).unwrap_err();
|
||||
assert!(!rgba.to_string().contains(CANARY));
|
||||
|
||||
// An extra trailing slot is read (CultLib's extra-trailing-slots ruling: read, never written
|
||||
// back). Whether a missing slot is refused is CultLib's rule, so no golden asserts it.
|
||||
let mut answered = invocation();
|
||||
answered.invocation_id = "inv-1".into();
|
||||
let extra: CommandResult = rmp_serde::from_slice(&committed("result-extra-slot")).unwrap();
|
||||
assert_eq!(extra, CommandResult::pending(&answered, seq(1), "working"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_tree() {
|
||||
// Decoding a Node costs tens of KiB of debug stack per nesting level, so the chain runs on a
|
||||
// thread that has the room; the 2 MiB default does not. The hazard is CultCache's, owned by
|
||||
// cultlib-gaps:follow_up:decode-stack-safety; the test stays on the big thread until it lands.
|
||||
std::thread::Builder::new().stack_size(16 * 1024 * 1024).spawn(deep_tree_body).unwrap().join().unwrap();
|
||||
}
|
||||
|
||||
fn deep_tree_body() {
|
||||
// A 64-deep Box chain.
|
||||
let mut chain = text("leaf", "bottom");
|
||||
for depth in (0..64).rev() {
|
||||
chain = boxed(&format!("deep-{depth}"), vec![chain]);
|
||||
}
|
||||
let deep = SurfaceDocument {
|
||||
provider_id: ProviderId::new("provider.a"),
|
||||
label: seg("deep"),
|
||||
version: ver(1),
|
||||
title: "deep".into(),
|
||||
root: chain,
|
||||
commands: vec![],
|
||||
};
|
||||
// A surface of 1,000 nodes: the root, 998 texts and one box.
|
||||
let mut wide_children: Vec<Node> = (0..998).map(|index| text(&format!("t{index}"), "x")).collect();
|
||||
wide_children.push(boxed("tail", vec![]));
|
||||
let wide = SurfaceDocument {
|
||||
provider_id: ProviderId::new("provider.a"),
|
||||
label: seg("wide"),
|
||||
version: ver(1),
|
||||
title: "wide".into(),
|
||||
root: boxed("root", wide_children),
|
||||
commands: vec![],
|
||||
};
|
||||
|
||||
let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("deep-tree.cc");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
{
|
||||
let mut cache = CultCache::new();
|
||||
ThingContract.register_entries(&mut cache).unwrap();
|
||||
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&path)).unwrap();
|
||||
put(&mut cache, &deep.record_key(), &deep);
|
||||
put(&mut cache, &wide.record_key(), &wide);
|
||||
}
|
||||
let cache = open_store(&path);
|
||||
assert_eq!(cache.get::<SurfaceDocument>(&deep.record_key()).unwrap(), Some(deep));
|
||||
assert_eq!(cache.get::<SurfaceDocument>(&wide.record_key()).unwrap(), Some(wide));
|
||||
}
|
||||
1
crates/thing-contract/tests/goldens/.gitattributes
vendored
Normal file
1
crates/thing-contract/tests/goldens/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* -text
|
||||
42
crates/thing-contract/tests/goldens/README.md
Normal file
42
crates/thing-contract/tests/goldens/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# thing-contract goldens
|
||||
|
||||
Written only by `crates/thing-contract/tests/goldens.rs`, with `THING_BLESS=1` set on that test.
|
||||
Every other run compares byte for byte. The TypeScript and C# packages and core decode these files
|
||||
and are held to the answers below; none of them writes one.
|
||||
|
||||
Each `.bin` is the payload of one record: the positional slot array of its kind, as MessagePack.
|
||||
A union value is `[armKey, [armSlots...]]`; a unit arm is `[key, []]`. An optional member is nil
|
||||
when absent. Arm keys are fixed for v1.
|
||||
|
||||
## Values (decode to the constructed value, re-encode to the same bytes)
|
||||
|
||||
| File | Kind | Decodes to |
|
||||
|---|---|---|
|
||||
| `surface-all-arms.bin` | `gamecult.thing.surface.v1` | One surface with all 13 node arms (Box 0, Text 1, Image 2, Button 3, Toggle 4, Slider 5, TextInput 6 as both input kinds, Text 0 and Number 1, Select 7, Repeat 8, Overlay 9, Plot 10, Embed 11, Use 12), a Box chain four levels deep, every text, number and bool source arm, every Format arm, both state sources, both member steps, every dimension and colour arm, and two declared commands |
|
||||
| `composition-all-params.bin` | `gamecult.thing.composition.v1` | A composition with one parameter of each of the 7 kinds, whose body uses a composition with one argument of each of the 7 argument arms |
|
||||
| `invocation-all-inputs.bin` | `gamecult.thing.command_invocation.v1` | An invocation with a payload record of bin bytes and one captured input of each of the 4 input arms |
|
||||
| `result-pending.bin` | `gamecult.thing.command_result.v1` | Outcome Pending, seq 1 |
|
||||
| `result-completed.bin` | same | Outcome Completed with source version 8, drafts cleared, no effect |
|
||||
| `result-completed-open-surface.bin` | same | Completed with no source version and the OpenSurface effect |
|
||||
| `result-completed-open-external.bin` | same | Completed with the OpenExternal effect and a resume command |
|
||||
| `result-denied.bin` | same | Denied naming a node |
|
||||
| `result-denied-no-node.bin` | same | Denied with a nil node |
|
||||
| `result-failed.bin` | same | Failed |
|
||||
| `asset-catalog.bin` | `gamecult.thing.asset_catalog.v1` | Two assets |
|
||||
| `surface.cc` | single-file store | One record of each of the five kinds, written under the pinned clock `2026-01-01T00:00:00Z`; reading it and writing it again gives the same bytes |
|
||||
|
||||
## Refusals and tolerance
|
||||
|
||||
| File | Payload | Expected |
|
||||
|---|---|---|
|
||||
| `result-thing1-flat.bin` | a MessagePack map with named members, the shape a Thing 1 provider wrote for a command result | refused when stored as a command result; the error names the kind and the record key and never a value |
|
||||
| `node-unknown-arm.bin` | a surface whose root node has arm key 13, which Node does not declare | refused; the error names the kind and the record key, and decoded directly it names the union Node and the key 13, and never a value |
|
||||
| `outcome-unknown-arm.bin` | a result whose outcome has arm key 4, which Outcome does not declare | refused in the same way, naming the union Outcome and the key 4 |
|
||||
| `result-extra-slot.bin` | a Pending result with a fourth slot past the last one declared | read: it decodes to the Pending result with seq 1 (CultLib's extra-trailing-slots ruling: read, never written back) |
|
||||
| `result-seq-zero.bin` | a result whose seq slot is 0 | refused; the error names the kind and the record key, decoded directly it names Seq, and never a value |
|
||||
| `surface-version-negative.bin` | a surface whose version slot is -1 | refused in the same way, naming Version |
|
||||
| `surface-label-dotted.bin` | a surface whose label is `b.c` | refused in the same way, naming Segment: a label holds no dot, so a record key splits at its last dot |
|
||||
| `asset-extent-negative.bin` | an asset catalog whose first asset has width -5 | refused in the same way, naming Count |
|
||||
| `rgba-channel-out-of-range.bin` | the Rgba slot array `[256, 0, 0, 255]` | refused when decoded as an Rgba: a channel is a byte, 0 to 255 |
|
||||
|
||||
Domain refusals are the contract's rule, which the TypeScript and C# packages mirror: each is held to refuse these files, without the leaf set a Rust `u8` or a checked newtype has.
|
||||
BIN
crates/thing-contract/tests/goldens/asset-catalog.bin
Normal file
BIN
crates/thing-contract/tests/goldens/asset-catalog.bin
Normal file
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
”ªprovider.a¥icons‘•¤gear©image/png¯CANARY-7f3a91c2û
|
||||
BIN
crates/thing-contract/tests/goldens/composition-all-params.bin
Normal file
BIN
crates/thing-contract/tests/goldens/composition-all-params.bin
Normal file
Binary file not shown.
BIN
crates/thing-contract/tests/goldens/invocation-all-inputs.bin
Normal file
BIN
crates/thing-contract/tests/goldens/invocation-all-inputs.bin
Normal file
Binary file not shown.
1
crates/thing-contract/tests/goldens/node-unknown-arm.bin
Normal file
1
crates/thing-contract/tests/goldens/node-unknown-arm.bin
Normal file
|
|
@ -0,0 +1 @@
|
|||
–ªprovider.a¤main¯CANARY-7f3a91c2’
‘¯CANARY-7f3a91c2<63>
|
||||
|
|
@ -0,0 +1 @@
|
|||
“¥inv-1’‘¯CANARY-7f3a91c2
|
||||
Binary file not shown.
Binary file not shown.
1
crates/thing-contract/tests/goldens/result-completed.bin
Normal file
1
crates/thing-contract/tests/goldens/result-completed.bin
Normal file
|
|
@ -0,0 +1 @@
|
|||
“Ù$3f2b8c1e-5d44-4a6b-9a07-1c2d3e4f5a6b’“ÃÀ
|
||||
|
|
@ -0,0 +1 @@
|
|||
“Ù$3f2b8c1e-5d44-4a6b-9a07-1c2d3e4f5a6b’’«not allowedÀ
|
||||
1
crates/thing-contract/tests/goldens/result-denied.bin
Normal file
1
crates/thing-contract/tests/goldens/result-denied.bin
Normal file
|
|
@ -0,0 +1 @@
|
|||
“Ù$3f2b8c1e-5d44-4a6b-9a07-1c2d3e4f5a6b’’©not yours¢go
|
||||
BIN
crates/thing-contract/tests/goldens/result-extra-slot.bin
Normal file
BIN
crates/thing-contract/tests/goldens/result-extra-slot.bin
Normal file
Binary file not shown.
1
crates/thing-contract/tests/goldens/result-failed.bin
Normal file
1
crates/thing-contract/tests/goldens/result-failed.bin
Normal file
|
|
@ -0,0 +1 @@
|
|||
“Ù$3f2b8c1e-5d44-4a6b-9a07-1c2d3e4f5a6b’‘¥broke
|
||||
BIN
crates/thing-contract/tests/goldens/result-pending.bin
Normal file
BIN
crates/thing-contract/tests/goldens/result-pending.bin
Normal file
Binary file not shown.
BIN
crates/thing-contract/tests/goldens/result-seq-zero.bin
Normal file
BIN
crates/thing-contract/tests/goldens/result-seq-zero.bin
Normal file
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
<EFBFBD>§receipt†©commandId¥inv-1§command¢go¥state©completed§message¯CANARY-7f3a91c2sourceVersion«issuedAtUtc´2026-01-01T00:00:00Z
|
||||
Binary file not shown.
BIN
crates/thing-contract/tests/goldens/surface-all-arms.bin
Normal file
BIN
crates/thing-contract/tests/goldens/surface-all-arms.bin
Normal file
Binary file not shown.
BIN
crates/thing-contract/tests/goldens/surface-label-dotted.bin
Normal file
BIN
crates/thing-contract/tests/goldens/surface-label-dotted.bin
Normal file
Binary file not shown.
BIN
crates/thing-contract/tests/goldens/surface-version-negative.bin
Normal file
BIN
crates/thing-contract/tests/goldens/surface-version-negative.bin
Normal file
Binary file not shown.
BIN
crates/thing-contract/tests/goldens/surface.cc
Normal file
BIN
crates/thing-contract/tests/goldens/surface.cc
Normal file
Binary file not shown.
226
crates/thing-contract/tests/result.rs
Normal file
226
crates/thing-contract/tests/result.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
mod common;
|
||||
|
||||
use common::*;
|
||||
use cultcache_rs::{CultCache, CultCacheEnvelope, CultCacheRegistry, DatabaseEntry, decode_payload};
|
||||
use serde::Deserialize;
|
||||
use serde::de::IntoDeserializer;
|
||||
use serde::de::value::{Error as ValueError, I32Deserializer, I64Deserializer, StringDeserializer};
|
||||
use thing_contract::*;
|
||||
|
||||
const CANARY: &str = "CANARY-9d41c7e0";
|
||||
|
||||
fn other_invocation() -> CommandInvocation {
|
||||
let mut other = invocation();
|
||||
other.invocation_id = "an-invocation-no-constructor-can-guess".into();
|
||||
other
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_copies_invocation_id() {
|
||||
let invocation = other_invocation();
|
||||
let built = [
|
||||
CommandResult::pending(&invocation, seq(1), "m"),
|
||||
CommandResult::completed(&invocation, seq(2), None, false, None),
|
||||
CommandResult::denied(&invocation, seq(3), "r", None),
|
||||
CommandResult::failed(&invocation, seq(4), "r"),
|
||||
];
|
||||
for (index, result) in built.iter().enumerate() {
|
||||
assert_eq!(result.invocation_id(), "an-invocation-no-constructor-can-guess");
|
||||
assert_eq!(result.seq().get(), index as i32 + 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_constructor_builds_its_outcome_arm() {
|
||||
let invocation = other_invocation();
|
||||
let node = Some(nid("go"));
|
||||
assert!(matches!(
|
||||
CommandResult::pending(&invocation, seq(1), "wait").outcome(),
|
||||
Outcome::Pending(PendingOutcome { message }) if message == "wait"
|
||||
));
|
||||
let effect = Effect::OpenSurface(OpenSurfaceEffect { key: "k".into() });
|
||||
assert!(matches!(
|
||||
CommandResult::completed(&invocation, seq(1), Some(ver(5)), true, Some(effect.clone())).outcome(),
|
||||
Outcome::Completed(CompletedOutcome { source_version: Some(v), clear_drafts: true, effect: Some(e) })
|
||||
if v.get() == 5 && *e == effect
|
||||
));
|
||||
assert!(matches!(
|
||||
CommandResult::completed(&invocation, seq(1), None, false, None).outcome(),
|
||||
Outcome::Completed(CompletedOutcome { source_version: None, clear_drafts: false, effect: None })
|
||||
));
|
||||
assert!(matches!(
|
||||
CommandResult::denied(&invocation, seq(1), "no", node.clone()).outcome(),
|
||||
Outcome::Denied(DeniedOutcome { reason, node: n }) if reason == "no" && *n == node
|
||||
));
|
||||
assert!(matches!(
|
||||
CommandResult::failed(&invocation, seq(1), "broke").outcome(),
|
||||
Outcome::Failed(FailedOutcome { reason }) if reason == "broke"
|
||||
));
|
||||
}
|
||||
|
||||
fn surface(provider: &str, label: &str) -> SurfaceDocument {
|
||||
SurfaceDocument {
|
||||
provider_id: ProviderId::new(provider),
|
||||
label: seg(label),
|
||||
version: ver(1),
|
||||
title: String::new(),
|
||||
root: boxed("r", vec![]),
|
||||
commands: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn composition(publisher: &str, name: &str, version: i64) -> CompositionDocument {
|
||||
CompositionDocument {
|
||||
publisher: publisher.into(),
|
||||
name: seg(name),
|
||||
version: ver(version),
|
||||
params: vec![],
|
||||
body: boxed("r", vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
fn catalog(provider: &str, id: &str) -> AssetCatalog {
|
||||
AssetCatalog { provider_id: ProviderId::new(provider), catalog_id: seg(id), version: ver(1), assets: vec![] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_keys_are_built_from_the_fields() {
|
||||
let invocation = other_invocation();
|
||||
assert_eq!(invocation.record_key(), "thing:invocation:an-invocation-no-constructor-can-guess");
|
||||
assert_eq!(
|
||||
CommandResult::failed(&invocation, seq(3), "r").record_key(),
|
||||
"thing:result:an-invocation-no-constructor-can-guess.3"
|
||||
);
|
||||
assert_eq!(surface("p", "main").record_key(), "thing:surface:p.main");
|
||||
assert_eq!(composition("pub", "card", 2).record_key(), "thing:composition:pub.card.v2");
|
||||
assert_eq!(catalog("p", "icons").record_key(), "thing:asset_catalog:p.icons");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_keys_injective() {
|
||||
// The pair the old keys collided on: a dotted provider with a plain label, and a plain
|
||||
// provider with a dotted label. The second no longer exists.
|
||||
assert_eq!(surface("a.b", "c").record_key(), "thing:surface:a.b.c");
|
||||
assert!(Segment::new("b.c").is_err());
|
||||
assert_ne!(surface("a", "c").record_key(), surface("a.b", "c").record_key());
|
||||
assert_ne!(surface("a.b", "c").record_key(), surface("a", "bc").record_key());
|
||||
|
||||
// A composition splits at its last two dots: publisher, name, then the version.
|
||||
assert_eq!(composition("a.b", "c", 1).record_key(), "thing:composition:a.b.c.v1");
|
||||
assert_ne!(composition("a.b", "c", 1).record_key(), composition("a", "bc", 1).record_key());
|
||||
assert_ne!(composition("a", "c", 1).record_key(), composition("a", "c", 11).record_key());
|
||||
assert_ne!(composition("a", "c", 1).record_key(), composition("a", "c", 0).record_key());
|
||||
|
||||
// A catalog likewise, and under the asset_catalog prefix.
|
||||
assert_eq!(catalog("a.b", "c").record_key(), "thing:asset_catalog:a.b.c");
|
||||
assert_ne!(catalog("a.b", "c").record_key(), catalog("a", "bc").record_key());
|
||||
|
||||
// A result splits at its last dot, and the seq is digits.
|
||||
let mut inv = invocation();
|
||||
inv.invocation_id = "x.1".into();
|
||||
let first = CommandResult::failed(&inv, seq(2), "r").record_key();
|
||||
inv.invocation_id = "x".into();
|
||||
let second = CommandResult::failed(&inv, seq(12), "r").record_key();
|
||||
assert_eq!(first, "thing:result:x.1.2");
|
||||
assert_eq!(second, "thing:result:x.12");
|
||||
assert_ne!(first, second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_newtypes_refuse_out_of_domain() {
|
||||
// new, the one door.
|
||||
for bad in [0, -1, i32::MIN] {
|
||||
assert!(Seq::new(bad).is_err(), "Seq {bad}");
|
||||
}
|
||||
assert_eq!(Seq::new(1).unwrap(), Seq::FIRST);
|
||||
assert_eq!(Seq::new(i32::MAX).unwrap().next(), Seq::new(i32::MAX).unwrap());
|
||||
assert_eq!(Seq::FIRST.next().get(), 2);
|
||||
assert!(Version::new(-1).is_err());
|
||||
assert!(Version::new(i64::MIN).is_err());
|
||||
assert_eq!(Version::new(0).unwrap().get(), 0);
|
||||
assert!(Count::new(-1).is_err());
|
||||
assert!(Count::new(i32::MIN).is_err());
|
||||
assert_eq!(Count::new(0).unwrap().get(), 0);
|
||||
assert_eq!(Count::new(7).unwrap().get(), 7);
|
||||
assert!(Segment::new("").is_err());
|
||||
assert!(Segment::new("a.b").is_err());
|
||||
assert!(Segment::new(".").is_err());
|
||||
assert_eq!(Segment::new("ab").unwrap().as_str(), "ab");
|
||||
|
||||
// serde, the other door: rmp_serde and serde's own IntoDeserializer.
|
||||
for bad in [0, -1, i32::MIN] {
|
||||
assert!(rmp_serde::from_slice::<Seq>(&rmp_serde::to_vec(&bad).unwrap()).is_err(), "Seq {bad}");
|
||||
assert!(Seq::deserialize(I32Deserializer::<ValueError>::new(bad)).is_err());
|
||||
assert!(Count::deserialize(I32Deserializer::<ValueError>::new(bad)).is_ok() == (bad >= 0));
|
||||
}
|
||||
assert_eq!(Seq::deserialize(I32Deserializer::<ValueError>::new(1)).unwrap(), Seq::FIRST);
|
||||
assert!(rmp_serde::from_slice::<Version>(&rmp_serde::to_vec(&-1i64).unwrap()).is_err());
|
||||
assert!(Version::deserialize(I64Deserializer::<ValueError>::new(-1)).is_err());
|
||||
assert_eq!(Version::deserialize(I64Deserializer::<ValueError>::new(0)).unwrap().get(), 0);
|
||||
assert!(rmp_serde::from_slice::<Count>(&rmp_serde::to_vec(&-1i32).unwrap()).is_err());
|
||||
assert_eq!(Count::deserialize(I32Deserializer::<ValueError>::new(0)).unwrap().get(), 0);
|
||||
for bad in ["", "a.b"] {
|
||||
assert!(rmp_serde::from_slice::<Segment>(&rmp_serde::to_vec(bad).unwrap()).is_err(), "Segment {bad:?}");
|
||||
let deserializer: StringDeserializer<ValueError> = bad.to_string().into_deserializer();
|
||||
assert!(Segment::deserialize(deserializer).is_err());
|
||||
}
|
||||
let deserializer: StringDeserializer<ValueError> = "ab".to_string().into_deserializer();
|
||||
assert_eq!(Segment::deserialize(deserializer).unwrap().as_str(), "ab");
|
||||
|
||||
// Each writes exactly the plain leaf, so the bytes are the leaf's.
|
||||
assert_eq!(rmp_serde::to_vec(&seq(5)).unwrap(), rmp_serde::to_vec(&5i32).unwrap());
|
||||
assert_eq!(rmp_serde::to_vec(&ver(5)).unwrap(), rmp_serde::to_vec(&5i64).unwrap());
|
||||
assert_eq!(rmp_serde::to_vec(&count(5)).unwrap(), rmp_serde::to_vec(&5i32).unwrap());
|
||||
assert_eq!(rmp_serde::to_vec(&seg("ab")).unwrap(), rmp_serde::to_vec("ab").unwrap());
|
||||
|
||||
// The error names the domain and its bound, never the value.
|
||||
let message = Segment::new(format!("{CANARY}.x")).unwrap_err().to_string();
|
||||
assert_eq!(message, "Segment must be free of '.'");
|
||||
assert_eq!(Seq::new(-77).unwrap_err().to_string(), "Seq must be at least 1");
|
||||
assert_eq!(Version::new(-77).unwrap_err().to_string(), "Version must be at least 0");
|
||||
assert_eq!(Count::new(-77).unwrap_err().to_string(), "Count must be at least 0");
|
||||
assert_eq!(Segment::new("").unwrap_err().to_string(), "Segment must be non-empty");
|
||||
let error = Segment::new(format!("{CANARY}.x")).unwrap_err();
|
||||
assert_eq!((error.domain(), error.bound()), ("Segment", "free of '.'"));
|
||||
assert!(!format!("{error:?}").contains(CANARY));
|
||||
}
|
||||
|
||||
/// The result payload `[id, seq, [0, [message]]]` with the seq slot spelled by the caller.
|
||||
fn result_payload(seq_slot: i32) -> Vec<u8> {
|
||||
let pending = Outcome::Pending(PendingOutcome { message: CANARY.into() });
|
||||
rmp_serde::to_vec(&("inv-1", seq_slot, &pending)).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_decode_door_holds_domain() {
|
||||
// A seq the constructors cannot build is refused by every decode door.
|
||||
for bad in [0, -1, i32::MIN] {
|
||||
let payload = result_payload(bad);
|
||||
let message = rmp_serde::from_slice::<CommandResult>(&payload).unwrap_err().to_string();
|
||||
assert!(message.contains("Seq") && !message.contains(CANARY), "{message}");
|
||||
|
||||
let message = format!("{:#}", decode_payload::<CommandResult>(&payload).unwrap_err());
|
||||
assert!(!message.contains(CANARY), "{message}");
|
||||
|
||||
let mut cache = CultCache::new();
|
||||
ThingContract.register_entries(&mut cache).unwrap();
|
||||
let key = format!("thing:result:inv-1.{bad}");
|
||||
let envelope = CultCacheEnvelope {
|
||||
key: key.clone(),
|
||||
r#type: CommandResult::TYPE.into(),
|
||||
payload,
|
||||
stored_at: "2026-01-01T00:00:00Z".into(),
|
||||
schema_id: Some(CommandResult::TYPE.into()),
|
||||
};
|
||||
let message = format!("{:#}", cache.put_envelope::<CommandResult>(envelope).unwrap_err());
|
||||
assert!(message.contains("gamecult.thing.command_result") && message.contains(&key), "{message}");
|
||||
assert!(!message.contains(CANARY), "{message}");
|
||||
}
|
||||
|
||||
// The boundary decodes, and equals what the constructor builds.
|
||||
let mut answered = invocation();
|
||||
answered.invocation_id = "inv-1".into();
|
||||
let expected = CommandResult::pending(&answered, Seq::FIRST, CANARY);
|
||||
assert_eq!(rmp_serde::from_slice::<CommandResult>(&result_payload(1)).unwrap(), expected);
|
||||
assert_eq!(decode_payload::<CommandResult>(&result_payload(1)).unwrap(), expected);
|
||||
}
|
||||
201
docs/research/swarm-dashboard.md
Normal file
201
docs/research/swarm-dashboard.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Swarm dashboard: contract state, sources, first cut
|
||||
|
||||
Serves Eureka's ruling `swarm-dashboard-first-thing2-surface` (not readable from
|
||||
the libby mind; cited by the id in the claiming follow-up
|
||||
`libby-body:follow_up:chat-s354-359`). Read-only pass by the `libby` Self run
|
||||
`libby:run:mind-20261031T061423908Z`, 2026-10-31. Nothing was built or deployed.
|
||||
Claims below were probed on 2026-10-31 unless marked **unverified**.
|
||||
|
||||
The question the dashboard answers for a stranger: **is anyone working, and on what.**
|
||||
|
||||
## 1. Where Thing 2's surface contract stands
|
||||
|
||||
Probed against the forge `GameCult/Thing` (main `e119e76`, 2026-10-22) and its
|
||||
`hands/thing2-*` branches. The source of truth for cut status is the `thing2`
|
||||
campaign in the Eureka mind, which this pass cannot read; the prose map
|
||||
(`docs/thing2-map.md`, last edited 2026-10-12) is the evidence used.
|
||||
|
||||
| Piece | State |
|
||||
| --- | --- |
|
||||
| Contract, Rust (`crates/thing-contract`) | On main. 13-arm closed `Node` union, `SurfaceDocument`, `CompositionDocument`, `CommandDecl/Invocation/Result`, `AssetCatalog`; byte goldens under `tests/goldens/`. |
|
||||
| Contract, C# and TypeScript | Not on main. Branches `hands/thing2-contract-cs` (`3dadfc6`, 2026-10-14) and `hands/thing2-contract-ts` (`aeede04`, 2026-10-12). The map lists the cross-runtime store check and publication as waiting. |
|
||||
| Core (resolution, `thing.std` compositions, binding via the CultMesh port, Taffy layout, command dispatch) | Not built. No crate exists. Decoding Verse surfaces waits on `cultlib-gaps:follow_up:decode-stack-safety`. |
|
||||
| TUI backend | Not built. Planned first backend. |
|
||||
| Web backend and shell | Not built. |
|
||||
| Discovery/advertisement | Not a Thing kind (`thing2:ruling:advertisement-owner`); read through a core port; today's `gamecult.eve.provider_advertisement.v1` is unchanged. |
|
||||
|
||||
`README.md` says the same: the layout solver, text, accessibility tree, render
|
||||
commands and backends "are not built yet". **Nothing can render a Thing 2
|
||||
surface today.** The only live renderers are Thing 1's (browser oracle, Ghostlight's vendored lowering,
|
||||
the Nightwing browser reference), and Eve dashboards are quarantined (`gamecult.md`: no new Eve surfaces).
|
||||
|
||||
### What a provider must publish to render
|
||||
|
||||
From `docs/thing2-map.md` "The consumer contract" and `crates/thing-contract/src`:
|
||||
|
||||
1. A `SurfaceDocument` (`gamecult.thing.surface.v1`, key `thing:surface:<providerId>.<label>`,
|
||||
`version` bumped only on structure change). Closed nodes: `Box, Text, Image, Button, Toggle,
|
||||
Slider, TextInput, Select, Repeat, Overlay, Plot, Embed, Use`. A swarm board is `Box` + `Text` +
|
||||
`Repeat` (one row per instance) + `Use` of `thing.std` compositions (`card`, `metric`, `key-value`).
|
||||
2. The **bound state records** the document points at, under the provider's own schema and key. A
|
||||
`Bind` is `{provider?, schema, key, member path, format?}` plus `Item` inside a `Repeat`. A
|
||||
collection binds to `{provider, schema, key_prefix}` or a list member of one record. No
|
||||
expressions, no derived values: **the state's owner publishes derived values** (map rationale,
|
||||
P5). So the dashboard's "working / idle" must be a published field, not computed in the surface.
|
||||
3. Discovery: the provider is found through the existing advertisement, not a Thing record.
|
||||
4. Commands only if the surface acts. A read-only board declares none.
|
||||
|
||||
A provider needs no layout or renderer code. It needs the contract package in its language
|
||||
(Rust is the only one on main) and typed CultMesh state.
|
||||
|
||||
## 2. Sources (all typed state)
|
||||
|
||||
### 2a. Run documents, eight minds
|
||||
|
||||
Each mind is its own Huginn daemon (`HEARTBEAT_PERSONAS` in `/etc/gamecult/heartbeat/heartbeat.env`):
|
||||
eureka 17872, libby 17873, heimdall 17874, kiko 17875, aqua 17876, weksa 17877, nibu 17878,
|
||||
void 17879, all `rudp://10.77.0.1:<port>`. Probed: from this Self run,
|
||||
`EUREKA_INSTANCE=<name> HUGINN_ENDPOINT=<endpoint> huginn query '{"schemas":["epiphany.pipeline.run.v2"],"limit":200}'`
|
||||
answers on all eight, read-only, with caller `client unknown` (no per-client read identity yet).
|
||||
Counts that day: eureka 68, libby 78, heimdall 94, kiko 70, aqua 81, weksa 74, nibu 71, void 87.
|
||||
|
||||
`epiphany.pipeline.run.v2` (`huginn schema run`): `instance, label, operated_by (Operator|Mind),
|
||||
turn (PersonaTurn|SelfRun), host, started_on (a date), budget_usd, claims [PipelineRef], campaigns`.
|
||||
|
||||
What the dashboard can and cannot take from it:
|
||||
|
||||
- **Open/closed is not on the Run.** It is derived: a Run is closed when a `Resolution` with
|
||||
`subject` = the run is in force (`huginn query` prints `InForce` or `Resolved:<outcome>`).
|
||||
Resolution key `<instance>:resolution:run.<label>.n<seq>`. `huginn history <runId>` reads it.
|
||||
- **Start time is a date.** The label `mind-YYYYMMDDTHHMMSSmmmZ` encodes the start to the
|
||||
millisecond, but that is a convention in a string. The admission header (`huginn view`) carries
|
||||
`admitted <timestamp>`, and the Resolution's header gives the end. Both are header facts, not
|
||||
document fields. A dashboard needing start and end times wants them as typed fields.
|
||||
- **"Outcome" is nearly empty.** Of 78 libby runs, 77 resolved `Recorded` and one is in force.
|
||||
The meaning lives in free text: `reason "answered 1"` (55), `"answered 2"` (5), `"the connector
|
||||
refused the credential lease: UnknownCaller"` (3), `"unit not active"`, `"failed after 0
|
||||
answered..."`, plus rationales `The Persona unit's start ended.` (61) and `The Self run's carrier
|
||||
closed the run its claude left open.` (11). Two Self-run resolutions carry the run's whole result
|
||||
message inside `reason` (long prose, operator-facing detail). A public board
|
||||
must **never project `reason` or `rationale`**.
|
||||
- **"On what" is only on Self runs.** `claims` holds the follow-up or other document the run took;
|
||||
a `PersonaTurn` has no claims (its work is the chat room). The claimed `follow_up.item` is the
|
||||
operator's chat-relayed brief, free text, sometimes private. The typed handle (claim id, campaign
|
||||
slug, the claimed document's `repo`) is safe; the `item` is not.
|
||||
- **An open Run can be a dead Run.** Eleven libby Self runs were closed with "carrier closed the
|
||||
run its claude left open", so for some interval an in-force Run belonged to nobody. "Open" is
|
||||
"admitted and not yet resolved", not "a process is working".
|
||||
- **Dollar fields:** `budget_usd` is on every Run. Do not bind it.
|
||||
|
||||
Snapshot at 06:16 UTC: two Self runs in force, `libby:run:mind-20261031T061423908Z` (this one) and
|
||||
`void:run:mind-20261031T061543899Z` (claims `void-body:follow_up:chat-s335-340`); no Persona run
|
||||
in force in any of the eight.
|
||||
|
||||
### 2b. Heartbeat state and poll trace
|
||||
|
||||
Source: `Huginn/crates/mind-body/src/heartbeat.rs`. `mind-heartbeat` is one Idunn-managed unit, sole
|
||||
writer of `heartbeat.cc` (a single-file CultCache store under the unit's state directory). Two kinds:
|
||||
|
||||
- `eureka.heartbeat.initiative.v1`, key = instance slug: `initiative_usd, as_of, urgency, urgency_at,
|
||||
last_burned_run, filled_events, classified_seq, launched_run, launch_spend_usd,
|
||||
launch_spend_known, completions_through, launch_window`.
|
||||
- `eureka.heartbeat.poll_trace.v1`, key = zero-padded unix ms, written only for polls that changed
|
||||
something: `at, instances [InstanceTrace], launches [run label], burns [BurnTrace], classifier`.
|
||||
`InstanceTrace`: `instance, initiative_usd, rate_usd_per_h, urgency, urgency_age_s, busy,
|
||||
released, crossing_at, waited_s, idle_s, filled_by, fill_reason, note`.
|
||||
|
||||
Usable without dollars: `busy` (an in-force Persona run, from the mind), `released`, `crossing_at`,
|
||||
`waited_s`, `idle_s`, `filled_by`/`fill_reason` (`tag` or `name`), `note`, `launches`, `urgency`,
|
||||
`classifier`. These say **why nobody is working** (waiting for initiative, held by the envelope).
|
||||
Dollar fields to leave alone: `initiative_usd`, `rate_usd_per_h`, `launch_spend_usd`,
|
||||
`spend_usd`, and in the Initiative record the same names. The metering teardown removes them, and
|
||||
`initiative_usd` is the unit initiative itself is denominated in, so the heartbeat's own model will
|
||||
change under whatever binds this.
|
||||
|
||||
Gaps:
|
||||
|
||||
- **Not verified live.** `heartbeat.cc` is not readable from this Self run's account, and no
|
||||
`/var/lib/gamecult` path for it was visible. The shape above is from source and
|
||||
`eureka-body-map.md:534-540`, not from a read of the file.
|
||||
- **No wire.** The heartbeat writes a file; it does not serve one. A reader opening a single-file
|
||||
store while the heartbeat rewrites it has no stated safety (`FileStore::open` pulls the whole file
|
||||
per call). A dashboard source must be published state, not a second process reading the file.
|
||||
`mind-heartbeat timeline` prints the latest poll for a human.
|
||||
- The trace is written only when a poll "changed something", so its newest line can be old while
|
||||
the system is healthy. Staleness must come from the heartbeat's own published liveness, which
|
||||
does not exist as a field.
|
||||
|
||||
### 2c. Connector usage readout
|
||||
|
||||
Source: `ModelConnector/schemas/usage_readout.json`, `crates/model-connector/src/readout.rs`.
|
||||
`gamecult.model.usage_readout.v1`: `{schema_id, at_unix_ms, plan_quota: [{window, status,
|
||||
utilization_permille (nullable), resets_at_unix_ms, observed_at_unix_ms}]}`. Whole-plan windows, no
|
||||
per-caller field, **no dollars**: the safe source for "how much capacity is left".
|
||||
|
||||
Gaps: it is a connector operation (`usage_readout` on `gamecult.model.connector`), reached at the
|
||||
heartbeat's `MIND_CONNECTOR_ENDPOINT` (`tcp://127.0.0.1:4110`), a loopback port with a per-caller
|
||||
credential. Three libby Self runs were already refused with `UnknownCaller` on a lease, so a new
|
||||
caller needs registration before it can ask. It is a request/answer, not a document in a store. A
|
||||
dashboard needs a publisher that polls it and republishes it as a CultMesh record. `plan_quota`
|
||||
also rides on every lease grant. **Unverified:** the live values, since asking requires a caller
|
||||
credential this pass was not given.
|
||||
|
||||
### 2d. Not a source: the presence provider
|
||||
|
||||
CultLib's 10-29 presence provider attests Idunn-managed **service** liveness, not attention or work
|
||||
(`libby-body:follow_up:presence-attention-not-carried`). It can answer "is the heartbeat unit up".
|
||||
It cannot answer "is anyone working".
|
||||
|
||||
## 3. Smallest first cut
|
||||
|
||||
**Cut 1: one published read model, `swarm.work.v1`, from the Runs of the eight minds. No heartbeat,
|
||||
no usage, no renderer.**
|
||||
|
||||
One record per instance (eight, key `<instance>`), derived and published by a single projector
|
||||
(owner: the swarm-dashboard provider), holding only closed-set or id fields:
|
||||
|
||||
- `state`: `Working` (a Self run in force), `Turn` (a Persona run in force), or `Idle`.
|
||||
- `run`: label of the newest in-force run, its `turn`, its start (from the admission header), else the
|
||||
newest closed run's end.
|
||||
- `on`: the claim id(s) and campaign slug(s), no free text.
|
||||
- `observed_at` and the per-mind `as_of`, so a dead projector reads as stale.
|
||||
|
||||
This answers the stranger's question with typed state only. It deliberately omits `reason`,
|
||||
`rationale`, `item`, `budget_usd`, and every `*_usd` field.
|
||||
|
||||
The Thing 2 surface for it is a `SurfaceDocument` that is a `Repeat` over those eight records, one
|
||||
`thing.std.card` each, bound to `state` / `on` / the time fields. It can be authored and golden-tested
|
||||
with the Rust contract on main. It cannot be seen until core (2a) and a backend exist. That is the
|
||||
actual gating item, and it is not this cut's to work around with a Thing 1 or Eve surface.
|
||||
|
||||
Cut 2: add heartbeat liveness and the why-idle fields (`busy`, `released`, `waited_s`, `idle_s`,
|
||||
`note`, `launches`), once the heartbeat publishes them as typed CultMesh state (it currently writes a
|
||||
file only, and the dollar-denominated initiative model is under teardown).
|
||||
Cut 3: add the `plan_quota` windows once a publisher polls the connector under a registered caller.
|
||||
|
||||
### Forks for the operator (recommendations given)
|
||||
|
||||
1. **Who is the stranger?** If public, the board shows ids and campaign slugs only (cut 1 as
|
||||
written); "on what" in words needs a typed `public_label` that the claiming agent sets, not
|
||||
`follow_up.item`. Recommend ids only until that field exists.
|
||||
2. **Where does the projector live?** Recommend a small reader that queries the eight Huginns (all
|
||||
answer today) and publishes `swarm.work.v1`, rather than adding publication to `mind-heartbeat`,
|
||||
which is Idunn-managed with a single-file store and a dollar model in flux. Confidence moderate:
|
||||
the cost is a new daemon, which needs the independent-lifecycle account `gamecult.md` requires.
|
||||
3. **Is "open" enough?** Recommend marking `Working` only when a Run is in force **and** its
|
||||
carrier is attested (the Run was admitted by a launch that is still live); the data for that
|
||||
attestation is not in the Run. Until it is, say in the surface that `Working` means "admitted,
|
||||
unresolved".
|
||||
4. **Typed times.** Recommend asking Huginn for `started_at` and `ended_at` as Run/Resolution
|
||||
document fields, so no consumer parses labels or header facts.
|
||||
5. **Sequencing.** A stranger sees nothing until core and a backend land. If the swarm board should
|
||||
come earlier than the TUI backend, say so; the TUI is the planned first backend and gives agents
|
||||
a reader, but a stranger needs the web backend (map cut 4).
|
||||
|
||||
### Substrate gaps this pass felt
|
||||
|
||||
- The `swarm-dashboard-first-thing2-surface` ruling and the `thing2` campaign are in Eureka's mind,
|
||||
not readable from libby's: cut status here comes from a 2026-10-12 prose map.
|
||||
- `heartbeat.cc` unreadable from a Self run, and a file, not a served document.
|
||||
- No typed `ended_at` / `started_at` on Run; outcome is free text in `reason`.
|
||||
- No per-client read identity on the eight Huginns (`client unknown`).
|
||||
256
docs/research/thing2-consumer-census.md
Normal file
256
docs/research/thing2-consumer-census.md
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
# Thing 2.0 consumer census
|
||||
|
||||
Campaign: `thing2`. Target: `thing2:target:r1`. Ruling cited: `thing:ruling:wire-rename-freeze-fold`.
|
||||
Author: Eyes (agent `eyes-thing2-consumer-census`, session `self-2026-10-10c-eureka`), read 2026-10-10. Facts only; "(inferred)" marks a reading from patterns rather than a located line. No recommendation.
|
||||
|
||||
Purpose: size the floor of the four consumer-contract parts in `docs/thing2-target.md` (Thing Script documents, binding, commands with typed results, advertisement) from what consumers publish, bind and command today. It does not re-derive `docs/thing-campaign.md`; rows are cited as T (rename table), E1-E7 (edges), B1-B28 (body facts). Campaign B14 (102 eve ids, 24 repos carry the advertisement id), B15 (what Idunn runs), B16 (one Verse-wide discovery edge), B19-B24 stand and are not repeated.
|
||||
|
||||
## Method and limits
|
||||
|
||||
- Every repo read at its `origin` default ref (not the local branch), via `git grep` / `git show`. Heads read:
|
||||
Ymir `f8c0e39` (2026-09-29), CultLib `dbcb2a8e` (2026-10-10), EveUnity `92f0dbb` (2026-08-26), EveConformance `048ea2f` (2026-09-16), EveElectron `cba684c` (2026-09-13), EveFlutter `3511e5c` (2026-07-10), EvePlugins `c5a4a75` (2026-07-15), EveTui `01ea52f` (2026-07-10), AquaSynth `aed6525` (2026-09-13), Loki `b20fb98` (2026-06-13), repixelizer `06c1cf4` (2026-09-30), Sai `2e7be7e` (2026-07-10), Stonks `3f6e91c` (origin/codex/stonks-market-daemon, 2026-09-30; no origin/main default), StreamPixels `8d40541` (2026-10-03), Vili `8773c15` (2026-09-13), weksa `d16cff5` (2026-09-13), Bifrost `67072b2` (2026-10-10), Gjallar `6a92976` (2026-09-29), Odin `8e1de66` (2026-10-01), Ghostlight `327cbe3` (2026-10-10), Heimdall `7904e17` (2026-09-30), Hermodr `f8e6e84` (2026-09-06), Mimir `d88f2e6` (2026-09-14), Muninn `1d0ea0b` (2026-09-30), VoidBot `46d891b` (2026-09-13), Brokkr `f97d4dc` (2026-10-10), Aetheria `0caf1fa5` (origin/master, 2026-10-10), Eve `b7d154b` (2026-10-10), gamecult-ops `9aed01f` (2026-10-10; B15 pinned `e4042ee`).
|
||||
- "Kind" counts come from regex over publisher source (JSON/JS/Rust/C#/Python literals `kind: "x"`, `"kind": "x"`, and builder-call names such as `pane(`, `.Metric(`). The vocabulary used to recognise a kind is the union of the kernel fixtures and `eve-browser-lowering`'s `kind ===` tests (70 names). Counts are literal-occurrence counts in publisher files, not runtime node counts. Publishers that build node trees from computed strings or typed structs are undercounted (inferred). Test, doc, dist and fixture paths excluded unless stated.
|
||||
- "Advertisement fields filled" is a text match for the schema's property names in the publisher files; typed-struct publishers can hide fields from it (inferred).
|
||||
- Aetheria publishes nothing: `git grep` for `gamecult.eve`, `EveSurface`, `cultui` in non-markdown files of `origin/master` finds one Obsidian index blob only. Its surfaces come through AetheriaEve (frozen, out of scope per target).
|
||||
- Not located: Gjallar and Ghostlight do not appear in `scripts/idunn/idunn-deployment-targets.ps1` at `9aed01f` (Ghostlight has its own binding `idunn/yggdrasil/bindings/ghostlight.toml.in`; Gjallar's `gjallar.service` is as B15 states). Not re-derived.
|
||||
|
||||
## The kernel's own vocabulary (floor reference)
|
||||
|
||||
- Kernel fixtures: `web/fixtures/*.json` surface fixtures at Eve `b7d154b` hold 35 distinct node kinds over 12 fixture files (node = object with `id`, `kind` and props/children); 24 `card`, 34 `text`, 10 `metric`, 8 `input.number`, 7 `partition`, 7 `pane`, 7 `control.button`, 6 `surface`. Single-use kinds: `vn.stage`, `embed.norn`, `embed.tex`, `field.surface2d`, `graph`, `canvas.editor`, `dropzone`, `input.file`, `layer.cards`, `layer.embedded-surfaces`, `status.stage`, `panel.dialogue`, `text.dialogue`, `rail.actions`, `image.background`. (`thing2-target.md` says 41 kinds in the fixtures; the 35 here is the JSON surface fixtures only, `.eve` DSL fixtures use `card`/`text`/`list`.) Source: `/f/Projects/Eve` `web/fixtures/` (script `s/k1.py`).
|
||||
- Kernel authoring/lowering mentions 70 distinct kind names across `eve-browser-lowering/src/index.ts` (`kind ===` tests: `rail`, `row`, `column`, `grid`, `form`, `modal`, `dashboard`, `cockpit`, `world.scene2d`, `world.scene3d`, `world.entity3d`, `inventory.grid`, `inventory.item`, `inventory.drag_session`, `list.item`, `control.*`, `input.*`, `image.sprite`, ...), the fixtures, and the C# builder.
|
||||
- Kernel contract doc: `docs/surface-contract-v1.md:93-109` ("Kinds are semantic", primitives plus "domain kinds such as `vn.stage` when the provider needs richer semantics", fallback "through the kind path": `panel.dialogue` renders as `panel`); `:293-` bindings (`bindingName`, `writeCommand`, `captureBindings`, local drafts); `:255-` standard command names (`select`, `invoke`, `open-provider`, `story.*`, `style.patch`, `edit.value`, `toggle.visibility`, `transform.*`).
|
||||
|
||||
## 1. Per-consumer facts
|
||||
|
||||
Liveness key: LIVE = Idunn target running per B15 (cited, not re-derived); BUILD = build-time or library consumer; DORMANT = no deploy target and no commit within ~90 days of 2026-10-10 (or no target).
|
||||
|
||||
### Providers and Verse daemons (publishers)
|
||||
|
||||
**Ghostlight** `327cbe3` — LIVE (Yggdrasil, own binding; B15). Commits 90d: many (2026-10-10).
|
||||
- Publishes: `gamecult.eve.surface.v1` via `crates/ghostlight-dungeon/src/eve.rs` (1007 lines). Kind literals: `text` x7 (:173), `card` x3 (:188), `control.input.textarea` x6 (:271), `control.input.text` x5 (:257), `control.input.number` x1 (:433), `control.button` x1 (:826), `surface` x1 (:808), `heimdall.access_gate` (:119), `heimdall.identity` (:236). 9 distinct kinds, 2 domain (`heimdall.*`). Provider/surface ids in `mesh.rs` (12 id hits).
|
||||
- Binding: node `stateBindings` with `local_draft` entries `{targetProp:"value", pointerId:"ghostlight.local.<key>", sourceId:"eve.browser.local", schemaId:"gamecult.eve.local_draft.v1", routeKind:"in-process", bindingName, valueKind, accessMode:"local-draft", authority:"eve.browser"}` (`eve.rs:872-884`; 19 `stateBindings` hits, `eve.rs:259`). Capture of drafts: `captureBindings` sent under `payload.bindings` (`runtime.rs:144` comment). Relies on the renderer (vendored `vendor/eve` lowering via its own web client, B17) to hold drafts and resolve; server routes `/api/eve/{provider,surfaces/{id},commands,events}` (`runtime.rs:655-658`, from B17).
|
||||
- Commands handled (operation ids, `runtime.rs:845-1476`; request schema map `eve.rs:708-720`): `heimdall.auth.begin` (`heimdall.auth_begin_command.v1`), `heimdall.auth.complete` (`heimdall.auth_complete_command.v1`), `app.auth.logout` (`ghostlight.app_logout.v2`), `world.create` (`ghostlight.world_create.v4`), `world.approve` (`..._v0`), `world.activate`, `world.advance_time`, `world.seed` (`ghostlight.world_seed.v1`), `world.play` (`ghostlight.world_play.v0`). 9 operations. Envelope validated at `eve.rs:665-700` (invocation schema `gamecult.eve.command_invocation.v1`, `receiptSchema` must equal `gamecult.eve.command_result.v1`, payload schema must equal the operation's). Result: `gamecult.eve.command_result.v1` built by `eve.rs:734 command_result`.
|
||||
- Known rejected path (in source): `eve.rs:723-731` — a flat result is rejected whole by the browser ("invalid Eve command result: data must not have additional properties", no command completes) because the result schema admits exactly `schema, receipt, transientProjection, pluginPayload, draftDirective`. Schema: Eve `schemas/gamecult.eve.command_result.v1.schema.json` (`required: [schema, receipt]`).
|
||||
- Advertisement: 17 of 18 schema properties matched in `mesh.rs` (all 11 required); also `plugin` surface docs via Heimdall (below).
|
||||
- Test gap: four `#[ignore]` Eve-bridge tests run on no automated path (B17).
|
||||
|
||||
**Muninn (daemon + sleipnir)** `1d0ea0b` — LIVE (Starfire, Raven, Nightwing; B15).
|
||||
- Publishes: `crates/muninn-daemon/src/main.rs:2637-2750` (`publish_move_hue_eve_surface`): `card` (:2655), `dashboard` (:2687), `pane` x5 (:2695), `text` x4 (:2698); surface key `surface:muninn.telemetry.<host>.move-hue` (`:611`). `crates/sleipnir-daemon/src/main.rs`: `card` x3 (:1034), `pane` x7 (:1104), `dashboard` x2 (:1121), `input.binding-map` (:1376), `binding.target` (:1401), `text` x2 (:1459). 6 distinct kinds across both; all primitives (inferred: `input.binding-map` is a kernel lowering kind, `docs/surface-contract-v1.md`/`index.ts`).
|
||||
- Binding: records written to a CultCache node via `node.put(&surface_key, &surface)` with `EveSurfaceStateRecord` (`main.rs:2673-2753`); no stateBindings located (inferred: static snapshots republished each tick).
|
||||
- Commands: reads `gamecult.eve.command.v1` documents (`main.rs:785`), command names `muninn.capture_stream_command` (:2966), `muninn.move_light_command` (:2978), `muninn.set-move-hue-program` (:2986); actions `start`/`stop` with `pending`/`running` states (:1185-1191); supersede logic for stale capture commands (:1172-1282). Receipts: command document default id `"eve-command"` (:840).
|
||||
- Advertisement: 13 of 18 properties; `schemas` and `witnesses` (required) not matched (inferred: may be built elsewhere).
|
||||
|
||||
**VoidBot** `46d891b` — LIVE (swarm publisher on Yggdrasil; B15).
|
||||
- Publishes: `scripts/render-voidbot-swarm-dashboard.mjs` builds the tree with `eveNode(id, kind, props, children)`: `metric` x13 (:1449), `pane` x10 (:1411), `text` x8 (:1412), `list` x7 (:1484), `grid` x3 (:1455), `row` x2 (:1397), `control.button` x2 (:1495), `cockpit` (:1378), `rail` (:1400), `control.slider` (:1428). 10 distinct, all primitives. Provider ads in `scripts/export-voidbot-provider-advertisements.mjs`.
|
||||
- Binding: richest located use. Node props carry `stateBindings: [{targetProp:"text", pointerId:"globalHeat", sourceId:"voidbot.swarm_control_state:voidbot-swarm", schemaId:"voidbot.swarm_control_state.v1", routeKind:"cultmesh-rudp", routeDescription}]` (`render-...dashboard.mjs:1415-1421`). `statePointer`-family regex finds 175 hits repo-wide, mostly Persona/state-path code unrelated to Eve (inferred; the Eve use is the dashboard). Also `gamecult.eve.surface_state.v1` x9, `interface_binding.v1` x5 id mentions (e.json).
|
||||
- Commands: `scripts/serve-voidbot-swarm-cultmesh.cjs:159-193` — accepts `gamecult.eve.command.v1`, only `command.command === "swarm.set_heat"` for `providerId "voidbot.swarm"` (:161), range check 0.05-2 throws (:164), applies `globalHeat` and writes `gamecult.eve.command_receipt.v1` (:182-193). One command.
|
||||
- Advertisement: 9 of 18 properties; `serviceId`, `kind` (required) not matched.
|
||||
|
||||
**Vili** `8773c15` — LIVE (Raven; B15).
|
||||
- Publishes: `scripts/vili-daemon.mjs:422-449` `eveSurface()` — nodes use the legacy `type` key not `kind`: `panel` (:431), `stat` x7 (:434), `command` x2 (:441). `stat` and `command` are not kernel kinds (not in the 70-name vocabulary) (inferred legacy dialect; `command` is `{type:"command", method:"GET", href}`). Vili also serves `/eve/operator`, `/eve/deck` (:25, :256-308).
|
||||
- Binding: none. Commands: none through Eve; `gamecult.vili.command_boundary.v1` is its own schema (:85-96, :315-322). Advertisement: 9 of 18; `serviceId, kind, freshness, schemas, witnesses, commands` not matched.
|
||||
|
||||
**Stonks** `3f6e91c` (origin/codex/stonks-market-daemon) — LIVE (Starfire; B15).
|
||||
- Publishes: `src/stonks-daemon.cjs:623-720` builders `pane(` x5, `metricNode(` x14, `textNode(` x6, plus a `kind: "interface"` node (:661) that child-surface aggregators read (Gjallar `Program.cs:616` filters children on `kind == "interface"`). `gamecult.eve.surface_state.v1` document type (:195-199).
|
||||
- Binding: none located. Commands: `stonks.command_boundary.v1` own schema (:179-183); no Eve command handler. Advertisement: 10 of 18; required `serviceId, verseId, freshness, schemas, witnesses` not matched.
|
||||
|
||||
**weksa** `d16cff5` — LIVE (Starfire; B15).
|
||||
- Publishes: `scripts/weksa-daemon.mjs` `buildOperatorSurface` area (:619, :826, :1014): `panel`, `person`, `text`. Document types `gamecult.eve.surface_state.v1`, `interface_binding.v1`, `provider_advertisement.v1` (:395-423); witness `eve-surfaces.cc` (:160, :286). `person` is likely a weksa domain node (inferred).
|
||||
- Binding: `gamecult.eve.interface_binding.v1` document (:412); 18 `state_ref`/`statePath`-family hits (e.json). Commands: RUDP command server (`scripts/weksa-rudp-command.cjs`), single command `mimo voicedesign` (`weksa-daemon.mjs:69-71`, schema `WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID`), receipt via `handleMimoVoiceDesign`; not an Eve `command.v1` handler (inferred). Advertisement: 14 of 18; `freshness` unmatched.
|
||||
|
||||
**Gjallar** `6a92976` — LIVE (Nightwing `gjallar.service`; B15).
|
||||
- Publishes: `src/Gjallar/Program.cs` kind literals `dashboard` (:846), `pane` (:1599), `text` (:1610), `interface` (:810); reads child `interface` nodes (:616). `VerseState.cs:169` `dashboard`. 4 distinct, 1 non-kernel (`interface`).
|
||||
- Binding/commands: none located (0 command hits). Advertisement: 15 of 18; `commands` unmatched.
|
||||
|
||||
**Heimdall** `7904e17` — LIVE (Yggdrasil; B15).
|
||||
- Publishes: no surface; `gamecult.eve.plugin_advertisement.v1` (x5) with plugin id `HEIMDALL_ACCESS_PLUGIN_ID`, capability `heimdall.access_gate` (`src/access-plugin.ts:86`), ABI `gamecult.eve.plugin_abi.request.v1` (:63), plugin operations `describe/validate/project` (:40), transport `cultnet.operation.v0` (:33-37). This is the only live domain-kind capability besides Ghostlight's use of it.
|
||||
- Commands advertised: `heimdall.auth.begin`, `heimdall.auth.complete`, `app.auth.logout` (:56); envelope `gamecult.eve.command_invocation.v1`, receipt `gamecult.eve.command_result.v1` (:41-42). Private command receipts in Postgres (`src/store/schema.ts:70`).
|
||||
- Advertisement: 8 of 18; `serviceId, verseId, freshness, surfaces` unmatched.
|
||||
|
||||
**StreamPixels** `8d40541` — LIVE (Yggdrasil service + web; B15).
|
||||
- Publishes: advertisement only (`apps/service/src/verse-state.ts:32-134`), surface reference `eveSurfaceId: "streampixels.eve.service_status.v0"` (:134); no kind literals. Binding/commands: none located (0 hits). Advertisement: 8 of 18; `serviceId, verseId, freshness, schemas` unmatched.
|
||||
|
||||
**Odin** `8e1de66` — LIVE (Yggdrasil, Starfire, Raven; B15). Consumer/aggregator, emits none.
|
||||
- `crates/odin-core/src/documents.rs:10-12,155-183`: reads `gamecult.eve.surface_state.v1` (`EveSurfaceStateRecord`), `interface_binding.v1` (`EveInterfaceBindingCompatRecord`), `provider_advertisement.v1`. Rust readers drop unknown records (B10/B16).
|
||||
|
||||
**Hermodr** `f8e6e84` — target status `archived` though a compose service exists (B15). Role: lowering and command relay: `src/hermodr-daemon.cjs:37-52` document types `command.v1`, `command_receipt.v1` ("reconciled" state per README); `src/static-lowering.cjs`, `src/lower-surface.cjs` (no kind literals found; Hermodr lowers server-side, inferred). Subscription code present (17 `subscribe` hits).
|
||||
|
||||
**Bifrost** `67072b2` — blocked (B15); 66 commits in 90 days.
|
||||
- Publishes: `tools/provider-advertisement.mjs` `metricNode(` x9 (:859), `dashboard` (:914), `panel` (:984), `metric` (:993), `list` (:1001), `text` x2 (:1006), `Program.cs:229` `Text(` x24; `Features/Motions/MotionEveSurfaceService.cs`: `form` (:35), `text` x3 (:47), `panel` (:53), `card` (:100). 6 distinct, all primitives. Surface endpoints `/eve/tui`, `/eve/gui` (:321-322).
|
||||
- Commands: `bifrost.motion_command.v0` at `cultmesh://<svc>/commands/motion` (`MotionEveSurfaceService.cs:7`, `provider-advertisement.mjs:166,330`), `bifrost.bridge_action.v0`, discord-post commands (:323-326). These are Bifrost schemas, not `gamecult.eve.command*` (inferred from endpoint list). Binding: `interface_binding.v1` x2. Advertisement: 15 of 18; `serviceId` unmatched.
|
||||
|
||||
**Mimir** `d88f2e6` — Nightwing dashboard target `archived` (B15 / ops file :250-259).
|
||||
- Publishes: `src/Mimir.EveDashboard/Program.cs`: builder calls `Text(` x77 (:1198), `Pane(` x20 (:1197), `Card(` x7, `Metric(` x8, `Row(` x3; literals `avatar` (:3424). Own ids `mimir.eve_dashboard_state.v1`, `mimir.eve_operator_surface.v1`, `cultmesh.eve_surface.v0` (x3), `mimir.eve_sensor_observation.v1`, `mimir.eve_media_observation.v1`, `mimir.eve_dashboard_command.v1` (E1 per T). Binding: reload-heavy (274 `reload|poll` hits) (inferred polling dashboard). Advertisement: 7 of 18.
|
||||
|
||||
**repixelizer** `06c1cf4` — blocked (B15); 4 commits in 90 days.
|
||||
- Publishes: `src/repixelizer/eve_surface.py`: `partition` x5 (:214), `text` (:27), `card` (:38), `pane` (:72), `input.select` (:310), `control.range` (:342), `surface` (:443); fixture `web/fixtures/repixelizer.eve-surface.json` in Eve. 7 distinct, all primitives. `surface_state.v1` x2, `verse_state.py` x16 id hits. Commands/binding: none located. Advertisement: 8 of 18.
|
||||
|
||||
**Loki** `b20fb98` — DORMANT (not deployed; last commit 2026-06-13).
|
||||
- Publishes: `src/loki-daemon.mjs:517-622` `surface`, `panel` x2, `metric` x8 (call+literal), `text` x2, `inspector.kv` x4 (:492) (`inspector.kv` is not a kernel kind except in EveUnity UI Toolkit lowerer; inferred domain-ish). Advertisement 16 of 18 (`schemas` incl. `gamecult.eve.surface.v1` :389,446); `commands: []` (:453,456). Binding/commands: none.
|
||||
|
||||
**Sai** `2e7be7e` — DORMANT (not deployed; last commit 2026-07-10).
|
||||
- Publishes: `src/eve.js` emits the VN domain family: `vn.stage` (:301), `panel.dialogue` (:267), `text.dialogue` (:279), `rail.actions` (:285), `image.background` (:255), `layer.embedded-surfaces` (:262), `layer.sprites` (:263), `layer.cards` (:264), `embed.norn` (:117), `embed.tex` (:170), `control.button` (:216); plus `color`/`select` style controls (:76, :90). 11 domain-or-VN kinds; this is the only repo emitting `vn.*`. It also emits `surface-state`/`surface-command` envelopes (:290, :333).
|
||||
- Commands: `src/eve-sidecar.cjs:52` `story.continue`, `story.choose`, `story.jump`; capabilities `vn.stage` + same (:53); rejects with `respond(..., "rejected", error.message)` (:21); `receiptSchema: gamecult.eve.command_receipt.v1` (:95). Plugin advertisement `plugins/sai-vn.plugin.json`.
|
||||
|
||||
**AquaSynth** `aed6525` — DORMANT/undeployed; 8 commits in 90 days. `tools/export-provider-advertisement.mjs`: advertisement only (7 of 18), kinds `patch, compile, instrument, speech, operator` are provider sub-kinds, not node kinds (inferred). No surface builder, binding or Eve command. 133 `reload`, 33 `subscribe` hits are audio, not Eve (inferred).
|
||||
|
||||
**Brokkr** `f97d4dc` — not deployed (B15); 41 commits in 90 days (active). `brokkr-daemon/src/main.rs`: advertisement partial (5 of 18) with `surfaces`, `commands`; own command schemas `brokkr.unity.command_receipt.v0` (:284), `brokkr.blender.command_receipt.v0` (:320); operation kinds `transform`, `parent`, `material`, `active-state` (:2749-3083) (not Eve node kinds; inferred). No surface kind literals.
|
||||
|
||||
**Ymir** `f8c0e39` — not deployed (B15); 23 commits in 90 days. `src/Ymir.Core/YmirServicePublicationDocument.cs:124-134`: `GameCult.Eve.Surface.EveSurface.Create("ymir.physics.operator")` with 5 `.Metric(...)` calls; record key `surface:ymir.physics.operator` (:147). Advertisement: 3 of 18 (`providerId, title, updatedAtUtc`) (inferred: typed `YmirServicePublicationDocument` rather than Eve's advertisement type). `CommandLowerings = []` (:29,109). Uses the C# `GameCult.Eve.Surface` package (E5 ProjectReference).
|
||||
|
||||
### Library, build-time and test consumers
|
||||
|
||||
**CultLib** `dbcb2a8e` — BUILD. Carries Eve ids in: `packages/cultcache-ts/src/swarm-documents.ts` (16 hits; document types `surface_state`, `interface_binding`, `:83-100`), `packages/cultmesh-kotlin/.../EveDocuments.kt` (8), `packages/cultnet-ts/test/cultnet.test.ts` (17), `tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs` (6), `samples/eve-two-runtime/sample.mjs` (10, with `stateBindings` at :61), `samples/eve-browser-network/` (C# + TS), provider session tests. Mesh primitives: `CultMeshOperationInvocationDescriptor`/`CultMeshOperationPayload`/`bindingName` (`GameCult.Mesh/CultMeshPrimitives.cs:3387`, `cultmesh-ts/src/index.ts:319`, 39 hits). B19 covers compatible-id wire behaviour. No node-kind emission (0 kind-literal publisher files).
|
||||
|
||||
**EveUnity** `92f0dbb` — BUILD (renderer; Unity packages; 2026-08-26). See section 6. Commands: `ResolveOperation` builds `CultMeshOperationInvocationDescriptor` from node props (`EveUnitySceneSurfaceLowerer.cs:64`, `EveUiToolkitSurfaceLowerer.cs:495,510`); command receipts class with default `gamecult.eve.command_receipt.v1` (`EveUnityPlayableWorldLiveClient.cs:284,298`). Binding: world root reads props `statePointerId`/`worldStatePointerId`, `assetManifest`, `inputProfile`, `cameraRig`, `movementCommand`, `focusCommand` (`EveUnitySceneSurfaceLowerer.cs:121-128`); scene lowerer additionally reads kinds `field.volume3d` (:114), `field.particles3d` (:116). Live provider transport over CultMesh: `Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs` (41 id hits). Pins: UPM git URLs (B21).
|
||||
|
||||
**EveConformance** `048ea2f` — BUILD (test/parity tooling). `tools/parity/parity-manifest.json` (22 schema-id hits; schemas under `../Eve/schemas/`), `consume-export.mjs`, `run-parity.mjs`. Requires every runtime to report command schema `gamecult.eve.command_invocation.v1` (`consume-export.mjs:883,1118`, `run-parity.mjs:1471`). Lock file pins six family repos (B21).
|
||||
|
||||
**EveElectron** `cba684c` (2026-09-13), **EveTui** `01ea52f` (2026-07-10), **EveFlutter** `3511e5c` (2026-07-10), **EvePlugins** `c5a4a75` (2026-07-15) — BUILD / renderer repos. EveTui command schema `gamecult.eve.command_invocation.v1` (`src/eve-tui-shell.mjs:42`); EveElectron `src/cultmesh-provider-client.mjs:2,229` (invocation schema; `operationId`); EveFlutter `lib/main.dart:326`. Output grid schema `gamecult.eve.tui_grid.v1` (EveTui x8). EvePlugins: plugin ABI/endpoint ids only (`gamecult.eve.plugin_endpoint.v1`, `plugin_witness.v1`), 0 surface hits.
|
||||
|
||||
**Eve kernel fixtures/package** `b7d154b`: browser reference + `eve-browser-lowering`, C# `org.gamecult.eve.surface` (`EveSurfaceBuilder.cs`: 149 id/builder hits), Android `MainActivity.kt`, iOS `Sources/`. B-rows: B14, B17-B19, B24.
|
||||
|
||||
## 2. Binding: observed shapes (union)
|
||||
|
||||
| # | Shape | Where | Resolved by |
|
||||
| --- | --- | --- | --- |
|
||||
| BD1 | Node prop `stateBindings: [{targetProp, pointerId, sourceId, schemaId, routeKind, routeDescription}]` | VoidBot `render-voidbot-swarm-dashboard.mjs:1415-1421`; CultLib `samples/eve-two-runtime/sample.mjs:61` | renderer (inferred; no daemon pushes values into the tree) |
|
||||
| BD2 | `stateBindings` local draft: adds `bindingName, valueKind, accessMode:"local-draft", authority:"eve.browser"`; `schemaId gamecult.eve.local_draft.v1`, `routeKind in-process` | Ghostlight `eve.rs:872-884` | renderer holds draft; `captureBindings` -> `payload.bindings` (`runtime.rs:144`) |
|
||||
| BD3 | `writeCommand` on a binding; `captureBindings` on operations; "accepted results may request that exact bindings be cleared" (`draftDirective`) | `docs/surface-contract-v1.md:293-` ; command_result schema | renderer + provider |
|
||||
| BD4 | World root props `statePointerId`, `worldStatePointerId`, `assetManifest(Uri)`, `inputProfile`, `cameraRig`, `viewId`, `playerEntityId`, `movementCommand`, `focusCommand` | EveUnity scene lowerer `:121-128` | Unity host with CultMesh live transport |
|
||||
| BD5 | DSL binding primitives `var`, `collection`, `derive`, `bind` (compiled into the tree) | `docs/surface-contract-v1.md` (Eve DSL section); fixtures `reactive-composition.eve`, `huginn-cc-surface.eve` (`collection ...`, `list ... bind <collection>`) | browser reference `web/eve-dsl.js` |
|
||||
| BD6 | Whole-document republish: provider writes `gamecult.eve.surface_state.v1` records (`{providerId, version, updatedAt, surface}`) to a CultCache/CultMesh node and bumps `version` | Muninn `main.rs:2673-2753`, Vili, Stonks, weksa, Gjallar, Hermodr, Bifrost (`surface_state` id counts: VoidBot 9, Gjallar 7, Stonks 4, Bifrost 3, Odin 3, Hermodr 3) | Odin aggregates; renderer re-fetches/reloads on version change (inferred) |
|
||||
| BD7 | `gamecult.eve.interface_binding.v1` document (13 id mentions: VoidBot 5, CultLib 2, weksa 2, Bifrost 2, Odin 2) | `swarm-documents.ts:99`, weksa `:412`, Odin `documents.rs:11,173` | Odin reads as `EveInterfaceBindingCompatRecord` ("Compat" in the type name) |
|
||||
| BD8 | Embedded documents: node `embeddedDocuments:[{slotId, documentId:"cultmesh://norn/graphs/...", schemaId, presentationKind}]` | `web/fixtures/sai-vn-surface.json`; `surface-contract-v1.md:144-153` | renderer plugin (`embed.norn`, `embed.tex`) |
|
||||
| BD9 | Reload/poll: DOM reference has `reload` (poll) paths; Mimir 274 `reload|poll` hits, StreamPixels 59, Odin 34 (repo-wide, not Eve-specific; inferred) | n/a | per-process |
|
||||
|
||||
Daemons using no binding at all in their published tree: Muninn, Vili, Stonks, Gjallar, Bifrost, repixelizer, Loki, Ymir (BD6 only) (inferred from absence of `stateBindings`/`pointerId` in publisher files).
|
||||
|
||||
## 3. Commands: observed set (union)
|
||||
|
||||
| Command / operation id | Handler | Request schema | Result schema | Cite |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `heimdall.auth.begin` / `.complete` / `app.auth.logout` | Ghostlight (and advertised by Heimdall plugin) | `heimdall.auth_begin_command.v1`, `heimdall.auth_complete_command.v1`, `ghostlight.app_logout.v2` | `gamecult.eve.command_result.v1` | `eve.rs:708-713`, `access-plugin.ts:56` |
|
||||
| `world.create`, `world.approve`, `world.activate`, `world.advance_time`, `world.seed`, `world.play` | Ghostlight | `ghostlight.world_*` (v0/v1/v4) | `command_result.v1` | `eve.rs:714-719`, `runtime.rs:1272-1476` |
|
||||
| `swarm.set_heat` | VoidBot swarm publisher | `gamecult.eve.command.v1` payload `{value}` | `gamecult.eve.command_receipt.v1` | `serve-voidbot-swarm-cultmesh.cjs:159-193` |
|
||||
| `muninn.capture_stream_command`, `muninn.move_light_command`, `muninn.set-move-hue-program` | Muninn | `gamecult.eve.command.v1` | command receipt (shape inferred) | `main.rs:2966-2986` |
|
||||
| `story.continue`, `story.choose`, `story.jump` | Sai sidecar | `command.v1` (`eve.js:3`) | `command_receipt.v1` | `eve-sidecar.cjs:52,95` |
|
||||
| `style.patch`, `select`, `invoke`, `open-provider`, `edit.value`, `toggle.visibility`, `transform.*` | kernel standard names | `command_invocation.v1` | `command_receipt.v1` | `surface-contract-v1.md:255-` |
|
||||
| `motion` command, `bridge_action`, `discord-post` | Bifrost | `bifrost.motion_command.v0`, `bifrost.bridge_action.v0`, Bifrost discord schema | `bifrost.*_receipt` (inferred) | `provider-advertisement.mjs:323-330` |
|
||||
| `mimo voicedesign` | weksa RUDP server | `WEKSA_MIMO_VOICEDESIGN_COMMAND_SCHEMA_ID` | own receipt | `weksa-daemon.mjs:69-71` |
|
||||
| `transform`/`parent`/`material`/`active-state` | Brokkr (Unity/Blender bridges) | `brokkr.*` | `brokkr.unity.command_receipt.v0`, `brokkr.blender.command_receipt.v0` | `brokkr-daemon/src/main.rs:284,320,2749-3083` |
|
||||
| `mimir.eve_dashboard_command.v1` | Mimir | own | n/a | e.json |
|
||||
| Ymir: `CommandLowerings = []`; own typed session commands (`YmirStepSessionCommand` etc.) are not Eve commands | n/a | n/a | n/a | `YmirServicePublicationDocument.cs:29,109` |
|
||||
|
||||
Counts (id mentions in non-test, non-JSON source): `command_receipt.v1` 35 (Eve 13, CultLib 7, VoidBot 5, Ghostlight 4, Hermodr 3, EveUnity 2, Sai 1), `command_invocation.v1` 34 (Eve 14, EveConformance 6, Ghostlight 5, EveElectron 3, EveTui 3, EveFlutter 2, Heimdall 1), `command.v1` 29, `command_result.v1` 12 (Eve 6, Ghostlight 5, Heimdall 1).
|
||||
|
||||
Rejected or failing command paths located in source or schema:
|
||||
- Ghostlight `eve.rs:723-731`: flat result is rejected whole by the browser lowering's schema check (see section 1).
|
||||
- Sai `eve-sidecar.cjs:21,43`: `rejected` responses and `rejectedComponentKinds` when a requested kind is not owned.
|
||||
- Muninn `main.rs:4721-4768`: `rejected_stale/radius/bounds/continuity` counts (these are sensor tracking counters, not command rejections; listed so they are not mistaken, inferred).
|
||||
- VoidBot `swarm.set_heat` out-of-range throws (:164).
|
||||
- `thing2-target.md` Why section cites "July failures (views that had to reload, failing commands, schema-rejected results)"; the schema-rejected case is the Ghostlight one above (inferred correspondence; the July commits were not read).
|
||||
|
||||
## 4. Advertisement
|
||||
|
||||
Schema `schemas/gamecult.eve.provider_advertisement.v1.schema.json` in Eve: 18 properties; required 11: `schema, providerId, serviceId, verseId, title, kind, freshness, schemas, witnesses, surfaces, commands`; optional `rootVerse, canonicalService, locatedService, cultMeshAddress, updatedAt, updatedAtUtc, conformanceScenarios`. Fixtures: `web/fixtures/*.provider-advertisement.json` (sai-vn, repixelizer, epiphany-model-atlas, eve-world-smoke) carry also `requiresPlugins[]` per surface entry with `pluginId, versionRange, availability, requiredCapabilities`.
|
||||
|
||||
Fields matched per publisher (matched / 18; text-match, so typed-struct publishers may be undercounted, inferred):
|
||||
|
||||
| Publisher | matched | required-not-matched |
|
||||
| --- | --- | --- |
|
||||
| Ghostlight | 17 | none |
|
||||
| Loki | 16 | none |
|
||||
| Bifrost | 15 | serviceId |
|
||||
| Gjallar | 15 | commands |
|
||||
| weksa | 14 | freshness |
|
||||
| Muninn | 13 | schemas, witnesses |
|
||||
| Stonks | 10 | serviceId, verseId, freshness, schemas, witnesses |
|
||||
| VoidBot | 9 | serviceId, kind |
|
||||
| Vili | 9 | serviceId, kind, freshness, schemas, witnesses, commands |
|
||||
| Heimdall | 8 | serviceId, verseId, freshness, surfaces |
|
||||
| StreamPixels | 8 | serviceId, verseId, freshness, schemas |
|
||||
| repixelizer | 8 | serviceId, verseId, schemas, commands |
|
||||
| Hermodr | 8 | serviceId, verseId, freshness, schemas, witnesses |
|
||||
| Mimir | 7 | serviceId, verseId, freshness, schemas, witnesses |
|
||||
| AquaSynth | 7 | serviceId, verseId, freshness, commands |
|
||||
| Odin (reads) | 7 | n/a (consumer) |
|
||||
| Brokkr | 5 | providerId, serviceId, verseId, freshness, schemas, witnesses |
|
||||
| Ymir | 3 | most |
|
||||
|
||||
Other advertisement-adjacent ids: `plugin_advertisement.v1` x11 (Heimdall 5, CultLib 2, Eve 2, EveConformance 1, Odin 1), `plugin_abi.v1` x12 (EveUnity, EveElectron, EveTui 3 each), `mimir.eve_sensor_observation.v1` x9 (Mimir 3, Eve 3, Odin 2, CultLib 1; E1). Top-line id mention counts (non-test, non-JSON source): `surface.v1` 94 (Bifrost 15, Eve 15, Ghostlight 12, weksa 7), `provider_advertisement.v1` 72, `surface_state.v1` 40 (VoidBot 9, Gjallar 7), `interface_binding.v1` 13, 64 distinct eve-named schema ids found by this pass (B14's 102 includes JSON/docs paths this pass excludes).
|
||||
|
||||
## 5. Liveness summary
|
||||
|
||||
| Class | Consumers |
|
||||
| --- | --- |
|
||||
| LIVE Idunn targets (B15; ops `9aed01f` re-read for targets file: Odin x3 hosts, Stonks, weksa, Muninn x3, Vili, VoidBot, StreamPixels, Heimdall are listed with Status `idunn-managed`/`runtime-enforced`/`enforced`) | Odin, Ghostlight, Heimdall, VoidBot, StreamPixels, Muninn (Starfire, Raven, Nightwing), sleipnir, Vili, Stonks, weksa, Gjallar |
|
||||
| Blocked or archived targets (code present) | Bifrost (blocked), repixelizer (blocked), Hermodr (archived target), Mimir dashboard (archived), `eve-ipad`, `periwinkle` (blocked) |
|
||||
| Not deployed by any target | Sai (last 2026-07-10), Loki (2026-06-13), AquaSynth, Brokkr (active, 41 commits/90d), Ymir (23 commits/90d), Epiphany (excluded) |
|
||||
| Build-time / library | CultLib, EveUnity, EveConformance, EveElectron, EveFlutter, EveTui, EvePlugins, Eve kernel |
|
||||
| No Eve publication | Aetheria (surfaces via frozen AetheriaEve) |
|
||||
|
||||
## 6. Renderer-side (renderer repos only)
|
||||
|
||||
Renderer capability manifests (`eve-runtime-capability.json`, schema `gamecult.eve.runtime_capability.v1`) list `supportedFeatures`, `supportedPlugins[].capabilities` and `unsupportedPlugins`; none lists node kinds. Every manifest's `unsupportedPlugins` is `[]`. Kind coverage therefore comes from literal presence in renderer source (heuristic, section Method), not a declared matrix.
|
||||
|
||||
| Renderer | features (manifest) | plugin capabilities | node-kind names present in source (of 70) |
|
||||
| --- | --- | --- | --- |
|
||||
| Eve web reference (`web/`, `eve-browser-lowering`) | 8 (`embeddedDocuments, providerAdvertisements, odinProviderDiscovery, genericWorldDiscovery, providerAssets, stateBindings, styleTokens, authorityWitnesses`) | `fields.surface` 5, `sai.vn` 4, `norn.graph` 1, `tex.math` 1 | 56 |
|
||||
| Eve Android (`MainActivity.kt`, 1 file) | not read | not read | 35 |
|
||||
| Eve iOS (`Sources/*.mm`, 8 files) | not read | not read | 23 |
|
||||
| Eve C# surface package (builder/document) | n/a (authoring) | n/a | 27 |
|
||||
| EveUnity UI Toolkit | `embeddedDocuments` | `sai.vn` 4, `norn.graph` 1, `tex.math` 3 | 28 (incl. `world.scene2d/3d`, `inspector.kv`) |
|
||||
| EveUnity scene | 26 (`providerSurfaceSession, sceneGraphProjection, playableWorldProjection, unityGameObjectSceneSink, ...`) | `fields.surface` 6 (incl. `volume.raymarch`), `sai.vn` 4, `norn.graph` 1, `tex.math` 4 | 13 (+ `field.volume3d`, `field.particles3d` literals) |
|
||||
| EveFlutter | no manifest in repo | n/a | 35 |
|
||||
| EveElectron | 9 | `sai.vn` 4, `norn.graph` 1, `tex.math` 3 | 8 (bar, embed.norn, embed.tex, label, surface, surface.slot, text, vn.stage) — low; delegates to browser lowering (inferred) |
|
||||
| EveTui | 6 | `sai.vn` 4, `norn.graph` 1, `tex.math` 3 | 7 (embed.*, label, metric, surface.slot, text, vn.stage); output `tui_grid.v1`; delegates to lowering package (inferred) |
|
||||
| Hermodr lowering | n/a | n/a | 0 |
|
||||
| Ghostlight web client | n/a | n/a | 1 (`surface`); uses `vendor/eve` browser lowering (B17) |
|
||||
|
||||
Kinds in the kernel vocabulary that no live publisher emits (rows of section 7 with zero in the "live" columns) include every `vn.*`, `embed.*`, `layer.*`, `field.*`, `world.*`, `inventory.*`, `canvas.*`, `image.*`, `color.swatch`, `control.tabs/popup/toggle/select/choice-group/box/part` and `modal/form/options/progress/bar` (live publisher counts below).
|
||||
|
||||
## 7. Cross-table: kind x consumer (publisher literal counts)
|
||||
|
||||
Columns: GL Ghostlight, MU Muninn+sleipnir, VB VoidBot, VI Vili (legacy `type`), ST Stonks, WK weksa, GJ Gjallar, BF Bifrost, MI Mimir (call+literal), RP repixelizer, LK Loki, SA Sai, YM Ymir, KF kernel fixtures (JSON, 12 files). First six data columns after KF are LIVE except as noted: GL MU VB VI ST WK GJ are LIVE; BF MI RP blocked/archived; LK SA YM undeployed. `-` is zero. Domain = my classification of a kind owned by one provider/domain rather than a layout/display/input primitive (inferred).
|
||||
|
||||
| kind | class | GL | MU | VB | VI | ST | WK | GJ | BF | MI | RP | LK | SA | YM | KF |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| text | primitive | 7 | 6 | 8 | - | 6 | 1 | 1 | 5 | 78 | 1 | 2 | - | - | 34 |
|
||||
| pane | primitive | - | 12 | 10 | - | 5 | - | 1 | - | 21 | 1 | - | - | - | 7 |
|
||||
| card | primitive | 3 | 4 | - | - | - | - | - | 1 | 7 | 1 | - | - | - | 24 |
|
||||
| metric | primitive | - | - | 13 | - | 14 | - | - | 10 | 9 | - | 8 | - | 5 | 10 |
|
||||
| dashboard | primitive | - | 3 | - | - | - | - | 2 | 1 | - | - | - | - | - | - |
|
||||
| panel | primitive | - | - | - | 1 | - | 1 | - | 2 | - | - | 2 | - | - | 2 |
|
||||
| partition | primitive | - | - | - | - | - | - | - | - | - | 5 | - | - | - | 7 |
|
||||
| list | primitive | - | - | 7 | - | - | - | - | 1 | - | - | - | - | - | 4 |
|
||||
| grid / row / rail / cockpit | primitive | - | - | 3/2/1/1 | - | - | - | - | - | 3 (row) | - | - | - | - | rail 1 |
|
||||
| form | primitive | - | - | - | - | - | - | - | 1 | - | 8 (calls) | - | - | - | - |
|
||||
| surface | primitive | 1 | - | - | - | - | - | - | - | - | 1 | 1 | - | - | 6 |
|
||||
| control.button | primitive | 1 | - | 2 | - | - | - | - | - | - | - | - | 1 | - | 7 |
|
||||
| control.slider / control.range | primitive | - | - | 1 | - | - | - | - | - | - | 1 (range) | - | - | - | 1 (range) |
|
||||
| control.input.text / textarea / number | primitive | 5/6/1 | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| input.number / input.select | primitive | - | - | - | - | - | - | - | - | - | 1 (select) | - | - | - | 8/1 |
|
||||
| input.binding-map, binding.target | primitive | - | 1/1 | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| avatar | primitive | - | - | - | - | - | - | - | - | 1 | - | - | - | - | - |
|
||||
| interface (aggregate marker) | non-kernel | - | - | - | - | 1 | - | 1 | - | - | - | - | - | - | - |
|
||||
| stat, command (legacy `type`) | non-kernel | - | - | - | 7/2 | - | - | - | - | - | - | - | - | - | - |
|
||||
| person | domain (inferred) | - | - | - | - | - | 2 | - | - | - | - | - | - | - | - |
|
||||
| heimdall.access_gate, heimdall.identity | domain | 1/1 | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| inspector.kv | domain | - | - | - | - | - | - | - | - | - | - | 4 | - | - | - |
|
||||
| vn.stage, panel.dialogue, text.dialogue, rail.actions, image.background, status.stage | domain | - | - | - | - | - | - | - | - | - | - | - | 1 each (no status.stage) | - | 1 each |
|
||||
| layer.cards / layer.embedded-surfaces / layer.sprites, embed.norn, embed.tex | domain | - | - | - | - | - | - | - | - | - | - | - | 1 each | - | 1 each (no layer.sprites) |
|
||||
|
||||
Aggregates:
|
||||
- Distinct node kinds emitted by the 7 LIVE surface publishers (GL, MU, VB, VI, ST, WK, GJ): text, pane, card, metric, dashboard, panel, list, grid, row, rail, cockpit, control.button, control.slider, control.input.text, control.input.textarea, control.input.number, input.binding-map, binding.target = 18 kernel primitives; plus non-kernel `interface`, `stat`, `command` and domain `heimdall.access_gate`, `heimdall.identity`, `person` (inferred) = 24 names total, 2 to 3 domain.
|
||||
- Including blocked/archived/undeployed publishers (BF MI RP LK SA YM): adds partition, form, surface, input.select, control.range, avatar, inspector.kv, and the full Sai VN family (11 domain kinds). Union 40 names; domain kinds 14 (11 Sai + 2 heimdall + inspector.kv), all but heimdall tied to the dormant Sai or Loki.
|
||||
- The kernel fixtures (35 kinds) contain 15 single-use domain/embed kinds; their only repo-owned emitter in source is Sai (`vn.*`, `layer.*`, `embed.*`, `panel.dialogue` family) and, for `field.surface2d` / `world.*`, the kernel and EveUnity (provider side not located in the repos surveyed; Aetheria's emission is via frozen AetheriaEve).
|
||||
- Of the kernel's 70-name vocabulary, the live publishers emit 18 (26 percent). `inventory.*` (3 kinds) is emitted by no surveyed publisher (Eve's own `EveInventoryInteraction.cs` and tests only); `world.*` is consumed by EveUnity scene and emitted by none surveyed (inferred: Aetheria-side, frozen).
|
||||
- Binding union: BD1-BD9 above. Command union: section 3 table (7 distinct command families on the Eve `command.v1`/`command_invocation.v1`/`command_result.v1` envelopes; others use their own schemas).
|
||||
|
||||
## 8. Source notes and scars
|
||||
|
||||
- Scripts and per-pass outputs: `eyes-thing2-consumer-census/s/*.py` in the scratchpad (a.py repo refs, c.py kind literals, e.py id matrix `e.json`, f.py command hits, h.py/m.py located kinds, j.py renderer coverage `j.json`, k.py advertisement fields).
|
||||
- One shell mistake created a stray interactive Python process that wrote a 64 MB tool-result file (`C:\Users\Meta\.claude\projects\F--Projects-CultLib\4afe6932-3e75-4766-be3f-a62ca680f00c\tool-results\b6rc8vggm.txt`); the task was stopped; the file is harness-owned and left in place.
|
||||
- Kind-literal counting is regex-based and undercounts computed or typed-struct emission; the Mimir 114 builder-call figure and VoidBot numbers are the largest counts and the least uniform (inferred).
|
||||
- Not read: Epiphany (excluded by `operator-epiphany-out-of-scope`), AetheriaEve (frozen), Fensalir/Delvehold-class Eve consumers outside the named list, Android/iOS manifests.
|
||||
97
docs/research/thing2-contract-forks-prior-art.md
Normal file
97
docs/research/thing2-contract-forks-prior-art.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Thing 2.0: prior art for the contract forks (Imagination, 2026-10-10)
|
||||
|
||||
Forks served, all raised in `thing2:campaign:self`: `thing2:question:code-home`,
|
||||
`thing2:question:thing-script-form`, `thing2:question:advertisement-owner`,
|
||||
`thing2:question:port-cutover`. Agent `imagination-thing2-contract`, session
|
||||
`self-2026-10-10c-eureka`. Facts read today through `gh api repos/<r>/contents`
|
||||
(top-level and named subdirectories); **[background]** marks prior knowledge not
|
||||
re-fetched in this pass. It does not repeat `thing2-foundation-prior-art.md` or
|
||||
`thing2-text-input-a11y-prior-art.md`.
|
||||
|
||||
## 1. How established toolkits split their code across repos (fork `code-home`)
|
||||
|
||||
| Project | What one repo holds (read today) | What lives in other repos |
|
||||
| --- | --- | --- |
|
||||
| Slint (`slint-ui/slint`) | `internal/` = compiler, core, interpreter, `backends/` (android-activity, linuxkms, qt, selector, testing, winit), `renderers/` (anyrender, femtovg, skia, software); `api/` = cpp, node, python, rs, wasm-interpreter. `api/node` is a Rust napi crate (`Cargo.toml`, `build.rs`, `binding.cjs`) in the same tree. | none needed for the shipped language APIs |
|
||||
| egui (`emilk/egui`) | `crates/` = egui, epaint, emath, ecolor, eframe, egui-wgpu, egui-winit, egui_glow, egui_kittest (tests), egui_extras, egui_plot, demo app | community backends (Bevy, macroquad) live with their hosts **[background]** |
|
||||
| iced (`iced-rs/iced`) | core, runtime, widget, renderer, wgpu, tiny_skia, winit, graphics, futures, test, tester | none for first-party backends |
|
||||
| Xilem (`linebender/xilem`) | masonry, masonry_core, masonry_winit, masonry_testing, xilem, xilem_core, xilem_web | Parley and Vello are separate linebender repos (independently consumed libraries) |
|
||||
| AccessKit (org) | `accesskit` = schema crate, consumer, every platform adapter | `accesskit-c` (C bindings, CMake) and `accesskit-python` (maturin) are separate repos |
|
||||
| Flutter | `flutter/flutter` now holds `engine/` beside `packages/`; `flutter/engine` is archived (last push 2025-02-25). The engine was merged into the framework repo to end cross-repo "rolls" **[background on the reason]**. | plugins and federated platform packages live in `flutter/packages` **[background]** |
|
||||
| Adaptive Cards (`microsoft/AdaptiveCards`) | `source/` = android, dotnet, ios, nodejs, uwp, shared: every host renderer of one card schema in one repo; `source/nodejs` adds `adaptivecards-templating`, `ac-typed-schema`, the designer | hosts (Teams, Outlook) are not in the repo |
|
||||
| Taffy | one crate, `test_fixtures/` + generated tests | Python binding `stretchable` is a third-party repo |
|
||||
|
||||
Pattern: the schema, the core and the first-party backends and language bindings
|
||||
share one repo when they share one build system or can be built from one
|
||||
workspace. Separate repos appear where a binding needs a different packaging
|
||||
toolchain (AccessKit C/Python) or is an independently consumed library (Parley,
|
||||
Vello). Flutter is the counter-example in the other direction: two repos with a
|
||||
version roll between them were merged into one.
|
||||
|
||||
The operator's July concern (`thing2-foundation-prior-art.md` 1.1, 2026-07-10)
|
||||
names what made the Thing 1 monorepo risk a swamp: "every runtime build system,
|
||||
plugin fixture, platform SDK, and product demo starts sharing one git status".
|
||||
Under the Thing 2.0 target, plugin fixtures and per-renderer lowerers do not
|
||||
exist (invariants `compose-not-kind`, `closed-commands`); the remaining platform
|
||||
SDKs with their own toolchains are Unity (UPM, Unity Editor) and Flutter (pub,
|
||||
Android/iOS SDKs). Neither has a live consumer (census section 5: EveUnity and
|
||||
EveFlutter are BUILD only; Aetheria publishes nothing).
|
||||
|
||||
## 2. How surface documents are authored (fork `thing-script-form`)
|
||||
|
||||
- **Typed document published by services, rendered by hosts:** Adaptive Cards
|
||||
(JSON card schema; `ac-typed-schema` package read today; hosts render with
|
||||
their own HostConfig theme **[background]**). Data binding is a separate
|
||||
templating package (`adaptivecards-templating`, read today) that expands a
|
||||
template against data before render. Slack Block Kit: JSON blocks with builder
|
||||
libraries per language **[background]**.
|
||||
- **Text language compiled to a typed tree:** Slint `.slint`, compiled by
|
||||
`internal/compiler` and also loadable at run time by `internal/interpreter`
|
||||
(both read today as directories); QML, XAML **[background]**.
|
||||
- **Code builders only:** Flutter, SwiftUI, Jetpack Compose, egui, Xilem
|
||||
**[background]**. All are single-process: the tree never crosses a wire.
|
||||
- **Body:** Thing 1 has both. The text DSL `web/eve-dsl.js` (544 lines) parses
|
||||
`.eve` fixtures; its local mesh lets a button `append`/`set` state directly in
|
||||
the renderer and computes `derive ... count|latest` there (`eve-dsl.js:103-155`).
|
||||
The C# builder `packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs`
|
||||
(876 lines) and every provider's hand-built JSON (census) are the builder side.
|
||||
Ruling `thing:ruling:operator-dsl-thing-script` names the authoring DSL Thing
|
||||
Script and keeps the retained tree schema-named; target r1 calls the published
|
||||
typed documents Thing Script.
|
||||
|
||||
## 3. Who owns service discovery (fork `advertisement-owner`)
|
||||
|
||||
- Body: `gamecult.eve.provider_advertisement.v1` (329 lines, 18 properties,
|
||||
11 required) carries Verse discovery (`verseId`, `serviceId`,
|
||||
`canonicalService`, `locatedService`, `cultMeshAddress`, `freshness`,
|
||||
`schemas`, `witnesses`) beside UI facts (`surfaces`, `commands`). Odin reads it
|
||||
as `EveProviderAdvertisementRecord { value: Value }`
|
||||
(`Odin crates/odin-core/src/documents.rs`, origin/main): an untyped blob.
|
||||
CultLib's CultMesh runtimes declare no service-advertisement document of their
|
||||
own (`git grep -i advertis` over `src/` and `packages/cultmesh-ts/src`: only
|
||||
transport endpoint and certificate-pin mentions). So the Verse's only provider
|
||||
discovery document lives in the UI library's namespace.
|
||||
- Weksa also writes `gamecult.eve.interface_binding.v1` holding a third copy of
|
||||
its routes (`provider.endpoints`, `routes`) and its whole surface
|
||||
(`weksa-daemon.mjs:321-341`); Odin reads it as
|
||||
`EveInterfaceBindingCompatRecord { value: Value }`.
|
||||
- Prior art: app or service manifests are separate from UI payloads in the
|
||||
service-driven UI systems above: a Teams app manifest declares bots and
|
||||
commands, and cards are message content **[background]**; service registries
|
||||
(Consul, Kubernetes Services) are not owned by any UI toolkit **[background]**.
|
||||
|
||||
## 4. How a cutover crosses many live consumers (fork `port-cutover`)
|
||||
|
||||
- Thing 1 plan (campaign `thing` revision 5): one Verse window, every E1 carrier
|
||||
flips together, because a rename of one kind cannot be read under two ids
|
||||
without an alias (ruling `thing:ruling:operator-compat-rebuild` forbids it).
|
||||
- Expand/contract ("parallel change") and the strangler-fig pattern migrate
|
||||
consumers one at a time while the old and new paths coexist, then delete the
|
||||
old path when the last consumer leaves **[background: Fowler]**.
|
||||
- Body: Thing 2.0 kinds are not renames of Thing 1 kinds. The document shape,
|
||||
the command envelope and the binding model differ (see the map's model page),
|
||||
so an aggregator reading both is reading two kinds, not one kind under two ids.
|
||||
Live viewers today: Ghostlight's web client (vendored `eve-browser-lowering`,
|
||||
`ajv` in `web/package.json`) and the Nightwing browser reference
|
||||
(`nightwing-eve-browser-reference`, `Repo = "Eve"`, runtime-enforced, `Deploy =
|
||||
$null`, gamecult-ops `idunn-deployment-targets.ps1:262-272`).
|
||||
275
docs/research/thing2-foundation-prior-art.md
Normal file
275
docs/research/thing2-foundation-prior-art.md
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# Thing 2.0: foundation prior art (Eyes pass, 2026-10-03)
|
||||
|
||||
Read-only facts with evidence pointers, gathered for the operator's question. No
|
||||
conclusions or recommendations. Facts marked **[background]** come from prior
|
||||
knowledge and were not re-fetched in this pass; everything else was read today.
|
||||
|
||||
The operator's words, verbatim: "I'm thinking about Thing 2.0. I think we learned
|
||||
a lot about what not to do with Thing 1. I feel like doing it right means
|
||||
building on a solid foundation with predictable affordances. Something like
|
||||
https://github.com/nicbarker/clay maybe?"
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Thing 1 (Eve), what the record shows
|
||||
|
||||
### 1.0 What the record does and does not contain
|
||||
|
||||
- `F:\Projects\Eve\docs\thing-campaign.md` (1,352 lines, revision 4, 2026-10-02)
|
||||
is a **rename and site campaign** (Eve to Thing, the deck page on
|
||||
gamecult.org). It records no lessons about the UI machine itself. The eureka
|
||||
mind's `thing` campaign holds the same: rulings on naming, repo home, deck
|
||||
cuts (`thing:ruling:operator-*`, `thing:cut_spec:cut-deck-*`). No Eve
|
||||
architecture findings were found in the mind (semantic query, 48 matches, all
|
||||
rename/deck/substrate).
|
||||
- voidbot `search_history` (Discord) returned only 2019-2021 Aetheria UI chatter;
|
||||
nothing on Eve. The operator's Eve words below come from Codex session logs
|
||||
(`C:\Users\Meta\.codex\sessions\...`) and Claude transcripts.
|
||||
|
||||
### 1.1 Operator's own words (verbatim, with source)
|
||||
|
||||
| Date | Words | Source |
|
||||
| --- | --- | --- |
|
||||
| 2026-07-02 | "Aetheria view is still busted. All commands are failing, and the layouts are quite bad. Go look at the main menu, it's very confused" | `.codex/sessions/2026/07/02/rollout-2026-07-02T12-43-25-...jsonl` |
|
||||
| 2026-07-02 | "...deferred scalar field lowering needs to be an Eve feature, not an Aetheria Electron feature" | same file |
|
||||
| 2026-07-02 | "...doesn't Eve provide bindings to CultMesh reactive typed state? Eir shouldn't have to reload to display new contents" | `.codex/sessions/2026/07/02/rollout-2026-07-02T14-05-11-...jsonl` |
|
||||
| 2026-06-17 | "And if an Eve surface contains a pointer to daemon state, the UI runtime should resolve that automatically" | `.codex/sessions/2026/06/17/rollout-2026-06-17T22-22-21-...jsonl` |
|
||||
| 2026-07-10 | "...avoid a giant Eve monorepo as the long-term shape. It will feel good while everything is young, then become a swamp: every runtime build system, plugin fixture, platform SDK, and product demo starts sharing one git status. We are already seeing the smell." | `.codex/sessions/2026/07/10/rollout-2026-07-10T02-54-*.jsonl` (may be a pasted plan the operator endorsed; the message is in the user turn) |
|
||||
| undated (Claude) | "the login button returns invalid Eve command result: data must not have additional properties" | a Claude transcript under `C:\Users\Meta\.claude\projects\` |
|
||||
| undated (Claude) | "they're separate, Eve CultUI is the lowering target. I'm not sure about the state of the Unity package, it kinda fell behind in the rush to build Eve" | Claude transcript; also memory `F--Projects/memory` (Unity CultUI vs Eve CultUI note) |
|
||||
| 2026-10-03 (above) | AetheriaEve: "an abomination preserved for taxidermy" | Claude transcript |
|
||||
|
||||
### 1.2 Body facts (read today)
|
||||
|
||||
**Layout semantics are delegated to each host's native layout engine.**
|
||||
- The contract (`Eve/docs/surface-contract-v1.md` l.32, 78-110) is a retained
|
||||
component tree, `surface.root`, with semantic `kind`s; "Kinds are semantic,
|
||||
not HTML tags. Renderers lower them to native controls". Unknown kinds fall
|
||||
back along the kind path (`panel.dialogue` to `panel`).
|
||||
- Layout vocabulary (`Eve/docs/cultui-style-system.md` l.22, 80-140):
|
||||
partition-first tree, relative or absolute division, `size` in `auto`,
|
||||
`content`, px, percent, viewport units or `fr`; `min`/`max`; `clip`; `scroll`.
|
||||
- No shared layout solver exists. The browser lowering
|
||||
(`Eve/packages/eve-browser-lowering/src/index.ts`, 2,680 lines) maps layout
|
||||
props onto CSS flexbox (`flexDirection`, `flexWrap` at l.679, 1057-1059) and
|
||||
measures with `getBoundingClientRect`. Flutter (`EveFlutter/lib/main.dart`,
|
||||
1,512 lines, one file) and Unity UI Toolkit
|
||||
(`EveUnity/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs`,
|
||||
900 lines) each re-interpret the same props with their own widgets/engine.
|
||||
Unity UI Toolkit's engine "uses the layout principles of Yoga"
|
||||
(docs.unity3d.com/Manual/UIE-LayoutEngine.html).
|
||||
- `Eve/docs/cultmesh-streaming-ui-framework.md` l.19: "Browser renderer: owns
|
||||
canonical layout behavior, accessibility semantics"; l.120 "the canonical
|
||||
behavior still belongs to the browser Eve runtime". `renderer-parity.md`
|
||||
names the web reference "the first renderer that can be used as a practical
|
||||
comparison oracle". So the layout spec is, in practice, whatever Chrome's
|
||||
flexbox does with the browser lowering's mapping.
|
||||
|
||||
**Per-renderer component implementations.**
|
||||
- Each runtime carries its own lowerer for the full kind set: web TS (2,680),
|
||||
Flutter Dart (1,512), Unity UITK (900) plus Unity scene lowerer (932) plus a
|
||||
1,544-line Unity CultMesh live transport
|
||||
(`EveUnity/.../Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs`),
|
||||
iOS UIKit "EveCanvas", native Android/Kotlin, Fensalir Direct2D spike,
|
||||
EveTui (`EveTui/src/eve-tui-shell.mjs`, 297 lines). EveElectron 64 lines.
|
||||
- The contract keeps adding domain kinds: `inventory.grid`, `inventory.item`,
|
||||
`inventory.drag_session` as "standard Eve component kinds"
|
||||
(`surface-contract-v1.md` l.311-330), `vn.stage`, `embeddedDocuments` slots,
|
||||
scalar/vector field lowering, Japanese-capable font roles and TUI pixel roles
|
||||
(l.359-392). 41 distinct `"kind"` values appear across `Eve/web/fixtures`.
|
||||
- Operator, 2026-07-02, asked that deferred scalar-field lowering move from an
|
||||
Aetheria Electron feature into Eve (1.1).
|
||||
|
||||
**Parity is evidence-heavy and admitted uneven.**
|
||||
- `Eve/docs/renderer-parity.md`: "Parity is currently uneven by design." Target
|
||||
is "recognizable CultUI fidelity", not pixel identity. Table rows: Flutter
|
||||
"Needs native window capture and tighter text/style normalization"; iOS "Not a
|
||||
plugin projection acceptance target"; Android Kotlin "Not a plugin projection
|
||||
acceptance target"; Fensalir "External adapter spike, not active parity
|
||||
runtime".
|
||||
- `Eve/docs/parity-testing-harness.md` (681 lines): a manifest
|
||||
(`tools/parity/parity-manifest.json`) of fixtures x runtimes, PNG captures per
|
||||
platform (phone/tablet/desktop; SSH capture on Nightwing, Periwinkle, iOS),
|
||||
runtime capability manifests, and exported gap ledgers
|
||||
(`capabilityGaps[]`, `worldSurfaceLoweringGaps[]`,
|
||||
`runtimePluginProjectionGaps[]`, command-boundary coverage). Parity is
|
||||
asserted on features/claims and screenshots, not on computed geometry.
|
||||
- Eve git: 381 commits; 18 match layout/parity/wrap/overflow (e.g. `e81c81b
|
||||
Measure Sai web layout probe`, `c0c17be Add web layout probe evidence`,
|
||||
`c4cd5b5 Polish native Odin wall layout`).
|
||||
|
||||
**Live CultMesh loop.**
|
||||
- `renderer-parity.md`: web reference "Needs live Odin/CultMesh provider feed
|
||||
and command round-trip tests"; Android Flutter "Needs live provider picker and
|
||||
command transport beyond fixture assets"; iOS "still needs provider picker
|
||||
and command transport". The local provider list is a fixture catalog
|
||||
(`web/local-provider-catalog.json`); live discovery "still belongs to
|
||||
CultMesh/Odin advertisements".
|
||||
- Operator words on reload, auto-resolving state pointers, failing commands and
|
||||
a schema-rejected command result (1.1).
|
||||
|
||||
**Text and input.**
|
||||
- `Eve/docs/input-capabilities.md` exists; no Eve doc mentions IME, caret or
|
||||
screen readers (grep of `Eve/docs/*.md` for `ime|caret|accessib|aria|screen
|
||||
reader` hit only the "accessibility semantics" ownership line above).
|
||||
`control.input` is a kind (`surface-contract-v1.md` l.105). Text styling
|
||||
normalization is a named Flutter gap. Font fallback for Japanese is a
|
||||
contract requirement (l.372-387).
|
||||
|
||||
**Repo shape.** Eight repos (Eve, EveUnity, EveFlutter, EveElectron, EveTui,
|
||||
EvePlugins, EveConformance, AetheriaEve); last commits 2026-07 to 2026-09
|
||||
(`thing-campaign.md` B1). The Unity CultUI package in CultLib "fell behind"
|
||||
(operator) and is a separate lineage.
|
||||
|
||||
### 1.3 Agent inferences (not operator words)
|
||||
|
||||
- "The composition model learns from the old Unity CultUI without preserving its
|
||||
construction accident ... not vertical-first layout" (`surface-contract-v1.md`
|
||||
l.56-58, agent-written doc).
|
||||
- The repo-strategy text attributed in 1.1 to 2026-07-10 reads like an agent's
|
||||
plan; its authorship in the user turn is a fact, its origin is not.
|
||||
- Pairing of operator complaints with the absence of a shared solver (1.2) is
|
||||
this pass's juxtaposition, not a recorded diagnosis.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: layout and UI foundations
|
||||
|
||||
### 2.1 Clay (nicbarker/clay), read from README and GitHub API today
|
||||
|
||||
- **Licence** Zlib. Single header `clay.h`, C99/C++20, no dependencies. 18.2k
|
||||
stars, 279 open issues, last push 2026-05-20; latest release **v0.14,
|
||||
2025-06-06** (v0.13 2025-02, v0.12 2024-10). Pre-1.0.
|
||||
- **Model**: per frame, `Clay_SetLayoutDimensions` -> `Clay_SetPointerState` ->
|
||||
`Clay_UpdateScrollContainers` -> `Clay_BeginLayout` -> `CLAY()` declarations
|
||||
-> `Clay_EndLayout(deltaTime)` returns a sorted `Clay_RenderCommandArray`
|
||||
(README l.617-621, l.109). Immediate-mode declaration; Clay retains element
|
||||
state internally by ID (`CLAY_ID`, `CLAY_IDI` for loops, l.305-309). Memory is
|
||||
a caller-provided arena (`Clay_MinMemorySize`, `Clay_Initialize`); multiple
|
||||
contexts allowed (l.587-601).
|
||||
- **Layout model**: flex-like row/column (`layoutDirection`
|
||||
`CLAY_LEFT_TO_RIGHT`/`TOP_TO_BOTTOM`), `padding`, `childGap`, alignment;
|
||||
sizing `CLAY_SIZING_FIT | GROW | FIXED | PERCENT` with min/max. No CSS Grid,
|
||||
no wrap-to-next-row container in the sizing set.
|
||||
- **Render commands** (complete enum in README): `NONE, RECTANGLE, BORDER,
|
||||
TEXT, IMAGE, SCISSOR_START, SCISSOR_END, OVERLAY_COLOR_START,
|
||||
OVERLAY_COLOR_END, CUSTOM`. Text commands are emitted one per wrapped line
|
||||
(l.1034). Transitions API with `deltaTime` (l.7, l.489).
|
||||
- **Text measurement**: user callback `Clay_SetMeasureTextFunction(...)`
|
||||
returning width/height; "It is essential that this function is as fast as
|
||||
possible"; Clay caches measurements internally (l.643-649). Wrap modes:
|
||||
words, newlines, none. Shaping, fonts, bidi are the host's.
|
||||
- **Scroll and floating**: scroll containers via `Clay_UpdateScrollContainers`
|
||||
(wheel and drag, smoothing by deltaTime) and `Clay_GetScrollOffset`;
|
||||
floating elements with `attachTo` (e.g. `CLAY_ATTACH_TO_PARENT`) and
|
||||
z-index, outside normal flow (l.419).
|
||||
- **Input helpers**: `Clay_SetPointerState`, `Clay_Hovered()`,
|
||||
`Clay_OnHover(cb)`, `Clay_PointerOver(id)`. Hit boxes are "from the last
|
||||
frame"; README suggests running layout twice per frame if that matters
|
||||
(l.368-373). No keyboard focus model.
|
||||
- **Not present**: the README has no mention of text input/editing, IME,
|
||||
keyboard focus or accessibility (grep empty). The web HTML renderer
|
||||
`renderers/web/html/clay-html-renderer.html` contains no `aria`/`role`
|
||||
attributes (grep count 0).
|
||||
- **Bindings**: README "supported bindings": Odin (in-repo) and Rust
|
||||
(`clay-ui-rs/clay`; crate `clay-layout` 0.4.0, updated 2025-06-03, 3,779
|
||||
downloads). Other implementations: three Go ports. **No C#, TypeScript, Dart,
|
||||
Swift or Kotlin binding is listed**; the web path is the C header compiled to
|
||||
wasm with clang ("15kb uncompressed .wasm", README l.9).
|
||||
- **Renderers in repo** (`renderers/`): GLES3, SDL2, SDL3, cairo, playdate,
|
||||
raylib, sokol, termbox2, terminal, web (`canvas2d`, `html`, prebuilt
|
||||
`clay.wasm`), win32_gdi.
|
||||
- **Performance claim**: "Microsecond layout performance" (README l.5); no
|
||||
benchmark table in the README.
|
||||
|
||||
### 2.2 Taffy (DioxusLabs/taffy)
|
||||
|
||||
- MIT. 3.6k stars, pushed 2026-10-03, release v0.14.0 2026-08-24.
|
||||
- Implements CSS **Block, Flexbox and CSS Grid** (README l.15). Retained tree
|
||||
`TaffyTree<NodeContext>` with `new_leaf`, `new_with_children`,
|
||||
`compute_layout`, `layout(node)`; also a low-level trait API for hosts that
|
||||
own their own tree **[background: `LayoutPartialTree` etc.]**. Leaf measure
|
||||
functions supply text/intrinsic sizes; Taffy does no text layout (README
|
||||
benchmarks note: "require text layout, which neither yoga nor taffy
|
||||
implement").
|
||||
- Users listed in README: Servo, Blitz, Bevy, Takumi, iocraft (terminal UIs),
|
||||
Slint, Lapce/Floem, Zed/GPUI.
|
||||
- Bindings: Python (`stretchable`); C and WASM bindings are **WIP PRs**
|
||||
(#404, #394). No official C#/TS/Dart binding.
|
||||
- Tests: "Flexbox layouts are tested by validating that layouts written in this
|
||||
crate perform the same as in Chrome": HTML fixtures in `test_fixtures/`,
|
||||
`just gentest` drives Chrome via webdriver and generates Rust tests into
|
||||
`tests/generated` (CONTRIBUTING.md l.26-35).
|
||||
- README benchmarks vs Yoga (M1 Pro, layout only): comparable, each wins some
|
||||
(e.g. 1,000 nested nodes 329 µs Taffy vs 365 µs Yoga; 100k wide 247 ms vs
|
||||
136 ms).
|
||||
|
||||
### 2.3 Yoga (facebook/yoga)
|
||||
|
||||
- MIT. 18.9k stars, pushed 2026-10-01; latest release v3.2.1 2024-12-13; npm
|
||||
`yoga-layout` 3.2.1.
|
||||
- "An embeddable and performant flexbox layout engine with bindings for
|
||||
multiple languages." Repo top level: C++ core (`yoga/`), `java/`,
|
||||
`javascript/` (wasm), `Package.swift`; no C# directory today.
|
||||
- Users **[background]**: React Native, Litho, ComponentKit; Unity UI Toolkit
|
||||
follows Yoga's layout principles (Unity manual, read today).
|
||||
- Tests: `gentest/` (`gentest-driver.ts`, `fixtures/`, `test-template.html`)
|
||||
generates cross-language tests from browser-computed layouts, the same
|
||||
pattern Taffy copied. Flexbox only, no Grid.
|
||||
|
||||
### 2.4 Contrast, briefly
|
||||
|
||||
- **Morphorm** (vizia/morphorm): MIT, 334 stars, pushed 2026-08-21. Layout by
|
||||
stacking with "space" and "stretch" units rather than CSS rules
|
||||
**[background on the unit model]**; used by Vizia.
|
||||
- **egui** **[background]**: immediate mode, single-pass layout during
|
||||
declaration (sizes known only after drawing, hence "sizing pass" tricks);
|
||||
output is `epaint` clipped shapes/meshes to thin backends (wgpu, glow,
|
||||
web canvas via eframe); integrates AccessKit; text editing is in-crate.
|
||||
- **Slint**: uses Taffy (README list) under its own declarative language;
|
||||
renderers femtovg/Skia/software **[background]**.
|
||||
- **Flutter** **[background]**: retained RenderObject tree, "constraints go
|
||||
down, sizes go up"; the engine paints display lists via Skia/Impeller.
|
||||
On the web, CanvasKit draws to canvas while a parallel DOM semantics tree
|
||||
(`flt-semantics`) carries accessibility and a hidden DOM input element
|
||||
carries text editing and IME.
|
||||
|
||||
### 2.5 Pattern: typed retained document -> layout -> render commands -> thin backends
|
||||
|
||||
- Clay is the direct instance of the last two arrows: one solver, a closed
|
||||
10-type command list, backends that only draw rectangles/borders/text
|
||||
lines/images/scissors (`renderers/*`, including terminal and termbox2).
|
||||
- Taffy and Yoga cover the solver over a retained tree; the host owns the tree
|
||||
shape, the measure callbacks and painting.
|
||||
- Parity techniques found in primary sources:
|
||||
- Taffy and Yoga: **generated geometry tests from a reference browser**
|
||||
(`test_fixtures` + gentest), asserting computed boxes, not pixels.
|
||||
- Eve today: screenshot captures per device plus capability/gap ledgers
|
||||
(`parity-testing-harness.md`); no geometry or render-command assertion.
|
||||
- With a single solver emitting commands, the command array is a
|
||||
renderer-independent artifact that can be compared directly; no project
|
||||
read in this pass publishes such a golden-command suite (not found, not
|
||||
searched exhaustively).
|
||||
|
||||
### 2.6 Text input and accessibility for canvas-style renderers on the web
|
||||
|
||||
- **AccessKit** (BSD-3 and MIT/Apache files in repo; pushed 2026-09-29): a
|
||||
schema for an accessibility tree pushed by the toolkit; "only the platform
|
||||
adapter needs to retain a complete accessibility tree", which the README
|
||||
says suits immediate-mode toolkits. Released adapters: Android, iOS, macOS,
|
||||
Unix (AT-SPI), Windows (UIA). **Web adapter is listed under "Planned
|
||||
adapters": "web (for applications that render their own UI elements to a
|
||||
canvas)"**. C bindings (`accesskit-c`) and Python bindings exist.
|
||||
- **DOM mirroring** is the shipped web practice in Flutter web (2.4,
|
||||
background) and is the only web path visible here; Clay's own HTML renderer
|
||||
emits plain DOM without ARIA.
|
||||
- Text editing/IME: Clay, Taffy and Yoga all leave it to the host. Eve's
|
||||
browser lowering currently gets native inputs from the DOM it builds.
|
||||
|
||||
---
|
||||
|
||||
## Pointers
|
||||
|
||||
- Eve docs: `F:\Projects\Eve\docs\{thing-campaign,renderer-parity,parity-testing-harness,surface-contract-v1,cultui-style-system,cultmesh-streaming-ui-framework}.md`
|
||||
- Lowerers: `F:\Projects\Eve\packages\eve-browser-lowering\src\index.ts`, `F:\Projects\EveFlutter\lib\main.dart`, `F:\Projects\EveUnity\packages\org.gamecult.eve.unity-uitoolkit\Runtime\EveUiToolkitSurfaceLowerer.cs`, `F:\Projects\EveTui\src\eve-tui-shell.mjs`
|
||||
- Clay: github.com/nicbarker/clay (README, `renderers/`); Taffy: github.com/DioxusLabs/taffy (README, CONTRIBUTING.md); Yoga: github.com/facebook/yoga (`gentest/`); AccessKit: github.com/AccessKit/accesskit (README).
|
||||
294
docs/research/thing2-text-input-a11y-prior-art.md
Normal file
294
docs/research/thing2-text-input-a11y-prior-art.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# Thing 2.0: text shaping, text input/IME, focus, accessibility, GPU glyphs: prior art (Eyes pass, 2026-10-10)
|
||||
|
||||
Serves the fork **thing2-text-ownership**: who owns text shaping, text editing/IME, keyboard
|
||||
focus and accessibility under the ruling `thing:ruling:thing2-one-solver-taffy`
|
||||
(typed retained document, one Taffy solver, closed render-command list, thin backends).
|
||||
Options: (a) Thing owns all, backends only draw; (b) hybrid, solver owns geometry and
|
||||
backends place native controls/a11y nodes at solved rects; (c) something unnamed.
|
||||
Item 6 (GPU glyph rasterization) is an operator scope addition.
|
||||
|
||||
Read-only facts with evidence pointers. No recommendation. Does not repeat
|
||||
`thing2-foundation-prior-art.md` (Clay/Taffy/Yoga basics, AccessKit web "planned").
|
||||
Marks: **(inferred)** = my juxtaposition; **[background]** = prior knowledge, not re-fetched.
|
||||
Versions/dates are crates.io `max_stable_version`/`updated_at` and GitHub API, read 2026-10-10.
|
||||
|
||||
---
|
||||
|
||||
## 1. Rust text stacks that pair with Taffy
|
||||
|
||||
| Crate | Latest (date) | Licence | Role |
|
||||
| --- | --- | --- | --- |
|
||||
| parley | 0.12.0 (2026-10-09) | Apache-2.0 OR MIT (emoji crate MIT-only) | rich-text layout, selection/editing, IME primitives |
|
||||
| fontique | 0.12.0 (2026-10-09) | [background: same repo, Apache-2.0 OR MIT] | font enumeration, fallback |
|
||||
| cosmic-text | 0.19.0 (2026-04-22) | Apache-2.0 OR MIT | shaping + layout + editor in one crate |
|
||||
| harfrust | 0.14.0 (2026-10-04) | [background: MIT] | HarfBuzz port; shaping |
|
||||
| rustybuzz | 0.20.1 (2024-11-12; no release since) | [background: MIT] | older HarfBuzz port |
|
||||
| swash | 0.2.10 (2026-07-17) | [background: Apache-2.0 OR MIT] | scaling/rasterising |
|
||||
| fontdb | 0.24.0 (2026-07-29) | [background: MIT] | font DB |
|
||||
| taffy | 0.14.0 (2026-08-24) | MIT | layout |
|
||||
|
||||
- **parley** github.com/linebender/parley README: "provides an API for implementing rich text
|
||||
layout"; shaping by HarfRust, bidi by ICU4X, line breaking in Parley, "utilities for text
|
||||
selection and editing", fontique for fallback, skrifa for outlines, ICU4X for segmentation.
|
||||
753 stars, MSRV 1.88 (README). Repo tree has `parley/src/editing/{editor,selection}.rs` and
|
||||
`examples/editor/src/{accessibility,access_ids}.rs` (AccessKit text example). PRs: "Implement IME
|
||||
primitives for PlainEditor" #136 (closed 2025-01-21), "Add editor features required for Android IME"
|
||||
#334 (2025-06), IME baseline-jitter / text-doubling fixes #599, #601 (2026-04/05). Slint's
|
||||
`accessibility-text` feature enables `parley?/accesskit`.
|
||||
- **cosmic-text** github.com/pop-os/cosmic-text: 2.2k stars, 841 commits. README: HarfRust shaping,
|
||||
bidi, swash rendering, own font fallback built from Chromium/Firefox lists; roadmap lists text
|
||||
editing, selection, bidi selection, copy/paste, click detection (page does not say which are
|
||||
done). IME not mentioned. Cargo.toml: harfrust 0.5, skrifa 0.40, fontdb 0.24, unicode-bidi,
|
||||
unicode-linebreak, unicode-segmentation; features `no_std`, `wasm-web`.
|
||||
- **Who uses which** (manifests on `main`, read today):
|
||||
- **Bevy**: `bevy_ui` -> taffy 0.14, accesskit 0.25; `bevy_text` -> parley 0.11, swash 0.2.6.
|
||||
IME PRs #23795 / #23841 "Text input IME support" closed 2026-04-21; #24421 "Fix IME text input
|
||||
wrapping" closed 2026-06-06. [background: Bevy used cosmic-text before.]
|
||||
- **Slint**: Taffy 0.10, flexbox feature only (`internal/core/Cargo.toml`); `shared-parley` is a
|
||||
default feature; parley 0.11.1; `accessibility-text` feature; accesskit 0.24, accesskit_winit 0.33.
|
||||
- **Blitz** (DioxusLabs): Stylo (CSS) + Taffy (box layout) + Parley (text layout) per README;
|
||||
`blitz-dom` has `events/ime.rs` and `accessibility.rs`; feature `accessibility = ["accesskit"]`;
|
||||
`blitz-shell` = winit + AccessKit + muda. README: "beta ... many bugs and missing features".
|
||||
- **Xilem/Masonry**: README: winit, Parley/Fontique, AccessKit. `masonry_core` depends on accesskit
|
||||
and parley; widgets `text_area.rs`, `text_input.rs`. masonry/xilem 0.4.0 on crates.io are
|
||||
2025-10-29 (repo pushed 2026-09-14). README: "experimental".
|
||||
- **GPUI (Zed)**: `crates/gpui` depends on taffy and accesskit; `font-kit` default feature;
|
||||
per-platform text systems (`gpui_apple/src/text_system.rs`, `gpui_wgpu` with font-kit,
|
||||
`gpui_web/src/{text_system,canvas_text}.rs`). AccessKit PRs #54398, #56065, #64212, #64214 all
|
||||
closed (merged vs not not checked), latest 2026-09-24.
|
||||
- **COSMIC / iced**: cosmic-text is pop-os's own crate [background].
|
||||
- **iocraft** (terminal): Taffy 0.5.2 flexbox, unicode-width 0.1.13 (section 5).
|
||||
- **egui**: accesskit workspace dep; own text layout [background].
|
||||
- Taffy has no text layout; leaf measure functions supply sizes (prior-art doc 2.2).
|
||||
|
||||
## 2. How Taffy-based toolkits do text input and IME
|
||||
|
||||
- **Masonry/Xilem (desktop)**: IME flows from winit into `TextArea`. Issues in
|
||||
github.com/linebender/xilem: "Re-add IME support" #762 (closed 2024-11-30), "Ensure that IME events
|
||||
interact with focus as expected" #1004, "Improve IME area reporting and set IME area in
|
||||
TextArea" #785, open #1692 "TextArea: on Ime::Disabled use finish_compose()" (2026-03-26),
|
||||
baseline-jitter fixes #1716 / #1718 (2026-04). Editing buffer is Parley's `PlainEditor` (inferred
|
||||
from parley #136 + masonry text_area). `xilem_web` is a separate DOM backend (README: "a web backend
|
||||
and a Masonry backend"); no Masonry-on-canvas web build found.
|
||||
- **Blitz**: `blitz-dom/src/events/ime.rs` `handle_ime_event` sends `BlitzImeEvent` to the focused
|
||||
node's `text_input_data_mut().apply_ime_event(font_ctx, layout_ctx, event)`: text input state lives
|
||||
in the DOM node, not a native control. Shell is winit; README lists no web target.
|
||||
- **Bevy UI**: IME merged 2026-04/06 (above); accessibility in `bevy_a11y`.
|
||||
- **Slint**: docs.slint.dev/latest/docs/slint/guide/platforms/web: "Accessibility features (such as
|
||||
screen readers) are not available" (WebGL canvas, no DOM); Wasm uses Winit backend + FemtoVG;
|
||||
"running Slint in the browser is currently not recommended for building general-purpose web
|
||||
applications". Text input / IME on web not addressed on that page.
|
||||
- **GPUI web** (`crates/gpui_web/src/ime_mirror.rs` header): "The hidden `<textarea>` that connects
|
||||
browser IMEs to GPUI". It mirrors a window of document text (UTF-16 units each side of the
|
||||
selection) into the textarea because IMEs "decide what backspace, autocorrect, and suggestions mean
|
||||
by inspecting the focused editable element's value and selection"; every write makes the browser
|
||||
restart the IME connection, so writes are coalesced to one per event-loop turn.
|
||||
`canvas_text.rs` rasterises glyphs via OffscreenCanvas 2D (4096 px max raster dimension).
|
||||
- **egui web**: `crates/eframe/src/web/text_agent.rs`: "a hidden `<input>` element used to capture
|
||||
IME and mobile keyboard input events", opacity 0, pointer-events none, 1x1 px, parked over the
|
||||
canvas corner "so that focusing it can never scroll some other part of the page into view".
|
||||
PR #8273 (closed 2026-08-23): the agent created "dead zones" swallowing clicks right of the cursor.
|
||||
#8078 (2026-04) "Delegate handling of IME interruptions to integrations to fix virtual keyboard
|
||||
flickering on web". `eframe/src/web/screen_reader.rs` is Web Speech `speak()`, not an a11y tree.
|
||||
- **Flutter web (text)** `engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart`:
|
||||
a real `<input>`/`<textarea>`, transparent color and caret, positioned by `transform` from
|
||||
framework-sent geometry; per-browser strategies (iOS moves it off-screen and re-places after 100 ms;
|
||||
Firefox listens `keyup`/`select`; Safari desktop skips focus and blur). Comments cite flutter issues
|
||||
98817 (skip re-placing during IME composition), 136006, 71275 (Safari ignores zero-sized fields),
|
||||
155265, 189744 (iOS blur during caret drag drops keyboard), 180652 (keep real field in its semantics
|
||||
node for tab order), 178743 (iOS no scroll-into-view in iframes); a TODO calls focus pulling
|
||||
"grabby" (166857). File read to char 100000 of 111416.
|
||||
- Pattern: egui, GPUI web, Flutter web all use a hidden/transparent native text element for IME with
|
||||
toolkit-pushed geometry (inferred from the three files above). Slint says nothing.
|
||||
|
||||
## 3. Accessibility and EditContext
|
||||
|
||||
- **AccessKit** github.com/AccessKit/accesskit: BSD-3-Clause (GitHub API; some crates Apache-2.0,
|
||||
e.g. accesskit_xplat), 1.5k stars. accesskit 0.25.1 (2026-09-25), accesskit_winit 0.34.1,
|
||||
accesskit_unix 0.24.0. README released adapters: Android, iOS, macOS, Unix (AT-SPI), Windows
|
||||
(UIA). **Web still under "Planned adapters"**: "for applications that render their own UI elements
|
||||
to a canvas". No web-adapter issue found (search "web" returned #561 "How closely to follow the
|
||||
WAI-ARIA roles spec/naming?" open since 2025-05; #705 "Add a property to allow exposing DOM ID"
|
||||
closed 2026-08-25; #565 "iOS adapter" open; docs PRs #753/#760 "Document ARIA mappings"
|
||||
2026-07/08). Inference: groundwork for DOM mapping, not an adapter.
|
||||
- **Integrators with Taffy-driven trees**: Bevy (`bevy_a11y`), Blitz (`BaseDocument::build_
|
||||
accessibility_tree` -> `accesskit::TreeUpdate`; tests `accessibility_roles.rs`,
|
||||
`accessibility_hidden.rs`), Masonry, Slint (desktop only), GPUI (annotation API PRs), egui.
|
||||
Blitz ships `accesskit_xplat 0.2.1` (winit-free adapter) whose README warns that AccessKit
|
||||
developers "have noted that the approaches used by both this crates and accesskit_winit may not be
|
||||
the optimal way" (accesskit PR #656 comment).
|
||||
- **Flutter web semantics** docs.flutter.dev/ui/accessibility/web-accessibility: Semantics tree is
|
||||
translated to an accessible HTML DOM with ARIA roles; "For performance reasons, Flutter's web
|
||||
accessibility is not on by default"; enabled by an invisible `aria-label="Enable accessibility"`
|
||||
button or `SemanticsBinding.instance.ensureSemantics()`. No cost quantified. Open
|
||||
github.com/flutter/flutter issues read 2026-10-10: #194011 "Clip semantics DOM text labels to stop
|
||||
stealing taps" (2026-10-08); #192466 "[iOS Safari] Long Semantics label overflows its button and
|
||||
intercepts taps on a later sliver" (2026-10-08); #191529 "prefer pointer events over semantics tap
|
||||
for mouse clicks on web"; #194109 "Remove the a11y placeholder only after the gesture ends";
|
||||
#172206 "excludeSemantics breaks aria-label on flutter web input for TextFormField"; #98160
|
||||
"Enable accessibility" read by screen reader even when auto-enabled; #118723 focus ring disappears
|
||||
when enabled in Chrome; #77283 not navigable with vimium-c. Pattern: semantics DOM layered over the
|
||||
canvas intercepts pointers and needs clipping (inferred).
|
||||
- **EditContext** developer.mozilla.org/en-US/docs/Web/API/EditContext_API: "Limited availability",
|
||||
experimental, standards track. MDN browser-compat-data `api/EditContext.json`: Chrome 121+ (and
|
||||
Chromium derivatives), **Firefox not supported** (bugzil.la/1904161), **Safari not supported**
|
||||
(webkit.org/b/269922). MDN: you render the editable region yourself (canvas ok); EditContext holds
|
||||
text/selection/composition state; you must report selection and caret location (IME window
|
||||
placement) and apply format updates; MDN recommends an offscreen DOM view of the text for screen
|
||||
readers since canvas text is unreadable. Spec body: [background: W3C Editing WG draft].
|
||||
|
||||
## 4. Hosts that are not ours
|
||||
|
||||
### Unity (UI Toolkit TextField)
|
||||
- docs.unity3d.com/Manual/UIE-uxml-element-TextField.html: `keyboard-type`, `auto-correction`,
|
||||
`hide-mobile-input`, `select-all-on-focus`, `select-word-by-double-click`, `max-length`,
|
||||
`is-delayed`, `emoji-fallback-support`, inherited `language-direction`. Page says nothing on IME,
|
||||
shaping or the advanced text generator.
|
||||
- Legacy `Input.compositionString` / `compositionCursorPos` docs: built-in GUI draws composition; for
|
||||
your own GUI "you need to take care of displaying the string at the current cursor position" and
|
||||
report position via `compositionCursorPos`. Docs call the Input class legacy.
|
||||
- Issue tracker: IN-147103 "Japanese IME candidate window overlaps the input field" (Unity 6, Win11
|
||||
and macOS standalone; field type unstated); 1171328 predictive text far from field for CJK (fixed
|
||||
2019.4/2020.1); UUM-100552 Japanese suggestion clears TextMeshPro field (fixed 6000.0.51f1).
|
||||
Whether UITK TextField is affected is **not established**.
|
||||
- Local: EveUnity UITK lowerer (prior-art doc 1.2); no IME/caret doc in Eve.
|
||||
|
||||
### Flutter
|
||||
- `TextInputConnection` (api.flutter.dev/flutter/services/TextInputConnection-class.html): the
|
||||
framework sends the host `setEditingState(TextEditingValue)`, `updateConfig`,
|
||||
`setEditableSizeAndTransform(Size, Matrix4)`, `setComposingRect`, `setCaretRect`,
|
||||
`setSelectionRects`, `setStyle`/`updateStyle` (family, size, weight, direction, align),
|
||||
`requestAutofill`, `show`/`close`. This is the data a self-rendered field pushes to the platform
|
||||
IME. No wire format or mandatory subset given on the page.
|
||||
- `HtmlElementView` (docs.flutter.dev/platform-integration/web/web-content-in-flutter): reserves
|
||||
layout space for a DOM element; "underlying platform views might sometimes swallow pointer events
|
||||
before they can reach Flutter"; documented fix `pointer_interceptor` (transparent platform view
|
||||
behind overlapping widgets: buttons, drawers, dialogs, menus). Page silent on clipping, scroll,
|
||||
z-order, performance. flutter#115679 (open, updated 2025-10-14) "[web] (Potential) Performance
|
||||
regression when scrolling platform views".
|
||||
|
||||
### Overlay failure modes with sources
|
||||
- Click interception by hidden/overlapping elements: egui #8273; Flutter docs; Flutter #194011, #192466.
|
||||
- Focus loss / keyboard: Flutter text_editing.dart comments (189744, 155265, 166857).
|
||||
- IME restarts on value writes: GPUI `ime_mirror.rs` header.
|
||||
- Page scroll on focus: egui text_agent comment; Flutter iOS 100 ms re-placement; 178743.
|
||||
- Safari ignoring zero-sized fields: Flutter 71275.
|
||||
- Scroll sync / clipping of overlays in scrolled canvas content: only flutter#115679 found; no
|
||||
first-hand scroll-sync source (not searched exhaustively).
|
||||
|
||||
## 5. TUI cell measurement
|
||||
|
||||
- **iocraft** github.com/ccbrown/iocraft (0.9.1, 2026-09-04, 188k downloads; `packages/iocraft/Cargo.toml`):
|
||||
taffy 0.5.2 (flexbox + taffy_tree only; far behind 0.14), unicode-width 0.1.13, crossterm 0.29.
|
||||
`components/text.rs`: Taffy `measure_func` wraps content with `s.wrap(width)` and returns the max
|
||||
`line.width()` (UnicodeWidthStr); modes Wrap / NoWrap; `MinContent` wraps at width 1. `canvas.rs`:
|
||||
draws per character group, advances `x += value.width().max(1)`, clips in cells; doc: "Character
|
||||
groups containing only zero-width marks reserve one column"; special case for U+FE0F with a
|
||||
`compatibility()` callback adjusting padding. `text_input.rs` keeps `cursor_offset: State<usize>`;
|
||||
no IME handling seen. Grep for "grapheme" in text.rs / canvas.rs / text_input.rs found none; where
|
||||
character groups are segmented was not traced.
|
||||
- **ratatui** (not Taffy): layout via `kasuari` 0.4.9 (Cassowary), `unicode-width >= 0.2.0` (version
|
||||
pin explained in ratatui issue #1271), `unicode-segmentation`, `unicode-truncate 3`.
|
||||
- unicode-width 0.2.2 (2025-10-06); unicode-segmentation 1.13.3 (2026-06-01).
|
||||
- Local: `EveTui/src/eve-tui-shell.mjs` (297 lines); no cell-width logic read in this pass.
|
||||
- No second Taffy-based TUI toolkit examined.
|
||||
|
||||
---
|
||||
|
||||
## 6. GPU glyph rasterization (operator scope addition)
|
||||
|
||||
### 6a. Slug
|
||||
- **Patent**: terathon.com/blog/decade-slug.html: "effective today, I am permanently and irrevocably
|
||||
dedicating the Slug patent to the public domain", **2026-03-17**; US 10,373,352 (granted 2019, would
|
||||
have run to 2038); USPTO SB/43 disclaimer filed.
|
||||
- **Repo** github.com/EricLengyel/Slug ("Reference code for the Slug Algorithm"), 1,547 stars, pushed
|
||||
2026-04-15. Files: `SlugPixelShader.hlsl`, `SlugVertexShader.hlsl`, `LICENSE`, `LICENSE-MIT`,
|
||||
`LICENSE-APACHE`, `NOTICE`, `README.md`. **Licence detail**: `LICENSE` says dual MIT or Apache-2.0 at
|
||||
your option; shader headers say MIT, "Copyright 2017, by Eric Lengyel"; GitHub API reports
|
||||
`NOASSERTION`. README adds "may be freely used by anyone for any purpose ... If you do use this code
|
||||
in software that gets distributed in any way, then you are required to give credit." Blog says MIT.
|
||||
- **Shaders cover**: pixel shader `SlugRender` (curve texture 4096 wide, 4x16-bit float; band texture
|
||||
2x16-bit uint; horizontal and vertical bands per glyph; per-band curves sorted by max coordinate;
|
||||
winding number per ray). Vertex shader includes **dynamic dilation**, which JCGT 2017 did not cover
|
||||
(blog). Blog: multi-colour emoji drawn as stacked glyph layers each with a bounding polygon (replaced
|
||||
a loop-in-shader design); adaptive supersampling removed; dilation addresses tiny and large text; the
|
||||
optional band split optimisation removed; band data reduced to two 16-bit components. Quadratic
|
||||
Beziers only (lines encoded {p1,p2,p2}). **Hinting**: README: "This algorithm ignores font hinting";
|
||||
suggests picking size so `size * sCapHeight` is an integer. Repo holds shaders only: no layout,
|
||||
shaping, atlas or texture builder (README tips describe the CPU preprocessing the host must do).
|
||||
- **JCGT 2017**: Lengyel, "GPU-Centered Font Rendering Directly from Glyph Outlines", JCGT 6(2),
|
||||
jcgt.org/published/0006/02/02 (page did not extract; abstract paraphrased from a search result of
|
||||
the paper.pdf): antialiased text from outline data on the GPU, no precomputed textures or distance
|
||||
fields; fixes numerical-precision artifacts of prior art; avoids divergent branching; accurate under
|
||||
affine and projective transforms.
|
||||
- **Open implementations** (GitHub search 2026-10-10, not exhaustive):
|
||||
- gabdube/rust-slug-wgpu: MIT, 15 stars, pushed 2026-04-15, "demo, not a rust crate. The code is not
|
||||
production ready"; WebGPU with storage buffers instead of textures; dynamic dilation not ported.
|
||||
- zzstar17/ash-slug: MIT, 1 star, 2026-09-29: preprocessing for Slug shaders (Vulkan/ash).
|
||||
- countertype/three-text: MIT, 126 stars, 2026-07-04: Three.js/WebGL/WebGPU adapters, HarfBuzz
|
||||
(harfbuzzjs wasm) shaping, Knuth-Plass justification, glyph caching (Slug use not confirmed in
|
||||
the README lines read).
|
||||
- No published wgpu crate or WebGL2 reference port found.
|
||||
- **Commercial Slug Library** (sluglibrary.com): "Full source is included with every license" (source
|
||||
available, not open source); single-engineer licence one-time $1,500 USD, unlimited commercial
|
||||
products, free updates while solo. Beyond the algorithm: UTF-8 layout services (glyph positions);
|
||||
kerning, ligature replacement, combining-mark placement, character composition, alternates;
|
||||
OpenType stylistic alternates, small caps, oldstyle figures, sub/superscripts, fractions; full-colour
|
||||
emoji incl. skin tones and ZWJ sequences. **Bidi/RTL: not mentioned**; complex scripts not explicitly
|
||||
claimed; full shaping not claimed; dynamic effects and hinting not mentioned. APIs: Vulkan,
|
||||
D3D10/11/12, OpenGL 3.0-4.6, Metal, **WebGL2**, consoles. Blog names Activision, Blizzard, id, 2K,
|
||||
Ubisoft, Warner Bros., Insomniac, Zenimax, Adobe as licensees.
|
||||
|
||||
### 6b. Vello family (github.com/linebender/vello, Apache-2.0, 4.4k stars, pushed 2026-10-09)
|
||||
- README "Choose a renderer": **Vello CPU** (`vello_cpu` 0.3.0, 2026-10-02, 5.7M downloads) is "the most
|
||||
mature choice"; **Vello GPU** (`vello_gpu` 0.3.0, 2026-10-02, 4.8k downloads) does CPU-side path
|
||||
preprocessing, GPU rasterisation and compositing of sparse strips, "supports a native WebGL2 backend
|
||||
and a wgpu backend without requiring compute shaders", "intended to become the primary renderer for
|
||||
production GPU use cases as it matures; Vello CPU is currently overall more mature". **Vello
|
||||
compute** (`vello` 0.11.0, 2026-10-02, 983k downloads) lives under `research/`, "an experimental
|
||||
implementation for compute-capable GPUs".
|
||||
- `vello_hybrid` 0.2.0 (2026-08-07) is the earlier crate name of the sparse-strips GPU renderer
|
||||
(inferred from README + issue titles); 2026-09 open issues are WebGL polish (#1625, #1943, #1167).
|
||||
`vello_gpu/src/lib.rs` features: `wgpu` (default), `webgl` ("browser support, using GLSL"), `text`.
|
||||
- Text: `glifo` 0.4.0 (2026-10-02, Apache-2.0 OR MIT, 3.2M downloads): "APIs for efficiently rendering
|
||||
glyphs and paint styles"; README: "under rapid development. Consider it experimental for now".
|
||||
Takes glyphs + positions, caches rasterised glyphs in an **atlas** (`glifo/src/atlas/*`), has
|
||||
`colr.rs` (COLR colour glyphs), `HintCache`, `FontEmbolden`, optional `png` (bitmap glyphs); shares
|
||||
"the hinting instance and hinted advance" between shaper and renderer. `Scene::glyph_run` consumes
|
||||
runs. Parley (same org) produces runs (inferred; parley README does not name glifo).
|
||||
- GPU requirements: wgpu backend needs no compute shaders; WebGL2 backend is native GLSL; the compute
|
||||
renderer needs compute-capable GPUs (README wording).
|
||||
|
||||
### 6c. SDF / MSDF atlases (contrast)
|
||||
- **msdfgen** github.com/Chlumsky/msdfgen: MIT, 4,952 stars, pushed 2026-08-29: multi-channel signed
|
||||
distance field generator; MSDF uses "all three color channels" to reproduce sharp corners "almost
|
||||
perfectly"; `mtsdf` adds a true SDF in alpha. Per-glyph distance-field atlas sampled by a shader.
|
||||
- [background] Valve 2007 SIGGRAPH "Improved Alpha-Tested Magnification" introduced SDF text atlases;
|
||||
single-channel SDF rounds corners; MSDF fixes corners (Chlumsky thesis, CTU Prague 2015); none
|
||||
address hinting or very small sizes well; colour emoji need a separate bitmap/COLR path. Not
|
||||
re-fetched.
|
||||
- Contrast with Slug (README + blog above): Slug stores curve and band tables, not rasterised
|
||||
images; SDF/MSDF store pre-rasterised distance atlases.
|
||||
|
||||
---
|
||||
|
||||
## Pointers
|
||||
- Prior Eyes doc: `F:\Projects\Eve\docs\research\thing2-foundation-prior-art.md`
|
||||
- Ruling: eureka mind `thing:ruling:thing2-one-solver-taffy`
|
||||
- Sources: github.com/linebender/{parley,vello,xilem}; github.com/pop-os/cosmic-text;
|
||||
github.com/DioxusLabs/blitz (`packages/blitz-dom/src/{events/ime.rs,accessibility.rs}`);
|
||||
github.com/zed-industries/zed `crates/gpui_web/src/ime_mirror.rs`;
|
||||
github.com/emilk/egui `crates/eframe/src/web/text_agent.rs`;
|
||||
github.com/flutter/flutter `engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart`;
|
||||
github.com/AccessKit/accesskit; github.com/ccbrown/iocraft `packages/iocraft/src/{components/text.rs,canvas.rs}`;
|
||||
github.com/EricLengyel/Slug; terathon.com/blog/decade-slug.html; sluglibrary.com;
|
||||
jcgt.org/published/0006/02/02/; MDN EditContext_API; docs.slint.dev web platform guide;
|
||||
docs.flutter.dev web-accessibility and web-content-in-flutter.
|
||||
|
||||
## Gaps
|
||||
- No numbers for Flutter semantics DOM cost; `flt-semantics` structure not read.
|
||||
- No measured scroll-sync failure source for native-control overlays beyond flutter#115679.
|
||||
- Unity UITK TextField IME behaviour not established.
|
||||
- iocraft grapheme segmentation origin not traced; Slug licence README credit clause differs from the
|
||||
blog's plain "MIT".
|
||||
- No Masonry-on-web and no Taffy + AccessKit-web combination found.
|
||||
|
|
@ -1710,3 +1710,61 @@ the same day, before `repo-renames` lands, with post-rename repo names: an
|
|||
`OrgRepo` is a name, and the renamed repos exist by the time any of their
|
||||
cuts runs, because each held spec's `first` names `thing` cut `repo-renames`.
|
||||
Each has its own target r1 carrying r3's invariants and citing this file.
|
||||
|
||||
## Revision 6: hold (2026-10-10)
|
||||
|
||||
Imagination (`imagination-thing-hold-remap`, session
|
||||
`self-2026-10-10c-eureka`). Rulings: `thing:ruling:hold-renames-for-thing2`
|
||||
and `thing:ruling:thing2-one-solver-taffy` (both operator, today), under
|
||||
`thing:ruling:operator-compat-rebuild`. The task: establish what can land
|
||||
before a Thing 2.0 map names the surviving repos, without a held cut. Probe
|
||||
script `p1_idreach.py` is in the Self's scratchpad
|
||||
`imagination-thing-hold-remap/`. Refs are each local checkout's
|
||||
`origin/<default>` as last fetched (not refreshed today except Eve and
|
||||
gamecult-ops); code files only, `docs/`, `*.md` and `dist/` excluded.
|
||||
|
||||
**B25. Which repos carry each kernel schema id.** Of the 38 kernel schemas,
|
||||
5 have no carrier outside the kernel (`asset_catalog`, `entity_soa_view.v1`,
|
||||
`input_bindings`, `input_capability`, `operation_payload`). 25 ids are
|
||||
carried only by held repos (the family, CultLib, Sai, repixelizer). Every id
|
||||
on the live discovery edge E1 (`provider_advertisement`, `surface`,
|
||||
`surface_state`, `interface_binding`, `command`, `command_receipt`,
|
||||
`plugin_advertisement`, `plugin_abi*`) has at least one held carrier:
|
||||
CultLib, and Stonks, Vili or weksa. Only `command_result` (Ghostlight,
|
||||
Heimdall) and `local_draft` (Ghostlight) are live and touch no held repo.
|
||||
|
||||
**B26. CultLib is on the live edge, not only build-time.** `CultLib`
|
||||
`packages/cultcache-ts/src/swarm-documents.ts:83-99` declares
|
||||
`gamecult.eve.surface_state.v1` and `gamecult.eve.interface_binding`.
|
||||
`gamecult-ops/scripts/deploy-heimdall.sh:22-27,108-120` archives
|
||||
`packages/cultcache-ts` and `cultmesh-ts` from `/srv/build/CultLib` at a
|
||||
pinned `cultlib_commit` and builds them into Heimdall's release. Heimdall's
|
||||
spec says as much ("vendored CultLib refreshed after cultlib-wire"). So E5's
|
||||
"compat, build-time" row is wrong for CultLib: `cultlib-wire` (held,
|
||||
`thing-family`) is a precondition of `heimdall-wire` and of the window.
|
||||
|
||||
**B27. The held providers are live.** `gamecult-ops` `origin/main`
|
||||
(`9aed01f`) `scripts/idunn/idunn-deployment-targets.ps1` still lists Stonks
|
||||
(l.49), weksa (l.62) and Vili (l.114) as daemon targets with published
|
||||
health. StreamPixels runs on Yggdrasil (B15).
|
||||
|
||||
**B28. Nothing has landed on a wire branch.** No `origin/thing/wire` exists
|
||||
in Eve, VoidBot, gamecult-ops, Odin or Ghostlight; the only `thing/*`
|
||||
branches are the merged persona cuts.
|
||||
|
||||
**What follows.** `verse-window` cannot run while any E1 carrier is held, so
|
||||
every cut whose `first` says "merge inside the Verse window" (the three
|
||||
kernel cuts, `voidbot-wire`, the eight `thing-verse` cuts) could only sit on
|
||||
an unmerged `thing/wire` branch, and `kernel-prose` and `ops-prose` depend on
|
||||
the window. The `thing-verse` cuts are not named by the hold but are held by
|
||||
it in effect (B26, B27). The subsets that land with no live edge are the five
|
||||
kernel-only ids and the Android and iOS app ids (E7): they would leave the
|
||||
kernel in a mixed `eve`/`thing` namespace for as long as the hold lasts, and
|
||||
are the kinds Thing 2.0's closed render-command list is most likely to
|
||||
reshape or delete. No wire cut is re-specified; the fork is question
|
||||
`thing:question:wire-rename-under-thing2`.
|
||||
|
||||
One item does not depend on the rename: follow-up
|
||||
`thing:follow_up:stale-voidbot-ignore-and-exclude` (three `.gitignore`
|
||||
lines and one `--exclude` in Eve) names a held cut as its owner and can be
|
||||
fixed on Eve main by itself.
|
||||
|
|
|
|||
608
docs/thing2-map.md
Normal file
608
docs/thing2-map.md
Normal file
|
|
@ -0,0 +1,608 @@
|
|||
# Thing 2.0 map
|
||||
|
||||
Campaign `thing2` in the eureka mind. Imagination pass `imagination-thing2-contract`,
|
||||
session `self-2026-10-10c-eureka`, 2026-10-10. Target `thing2:target:r1`
|
||||
(`docs/thing2-target.md`). Rulings in force and cited by id:
|
||||
`thing:ruling:thing2-one-solver-taffy`, `thing:ruling:thing2-thing-owns-text`,
|
||||
`thing:ruling:wire-rename-freeze-fold`, `thing:ruling:operator-compat-rebuild`,
|
||||
`thing:ruling:operator-dsl-thing-script`, `thing:ruling:operator-member-names-keep`,
|
||||
`thing:ruling:operator-aetheria-eve-rename`, and the six `thing2:ruling:*` of
|
||||
2026-10-10 (`code-home`, `thing-script-form`, `advertisement-owner`,
|
||||
`port-cutover`, `campaign-repo-limit`, `retired-repo-names`).
|
||||
|
||||
This page holds body facts, the model page, the consumer contract at the level
|
||||
consumers need, the cut order at the level of deliverables, and rationale. The
|
||||
questions it raises are documents in the mind (`thing2:question:*`); it does not
|
||||
restate their options. Research for the forks:
|
||||
`docs/research/thing2-contract-forks-prior-art.md`. Evidence it builds on:
|
||||
`docs/research/thing2-consumer-census.md` (census, cited as C§n, BD1-BD9),
|
||||
`docs/research/thing2-foundation-prior-art.md`,
|
||||
`docs/research/thing2-text-input-a11y-prior-art.md`, and `docs/thing-campaign.md`
|
||||
(rename table T, edges E1-E7, body facts B1-B28).
|
||||
|
||||
## Body facts
|
||||
|
||||
Probes P1-P16 ran 2026-10-10, P17-P22 2026-10-12, against `origin/<default>` after `git fetch` unless a
|
||||
local path is named. Scripts are in the session scratchpad
|
||||
`imagination-thing2-contract/`.
|
||||
|
||||
**P1. The Thing 1 contract is 38 schemas, most of them process machinery.**
|
||||
`p1_schemas.py` over Eve `schemas/` at `a758787`: 38 files. Eleven describe
|
||||
conformance, hand-off, split-target, release, capture and witness bookkeeping
|
||||
(`conformance_*`, `*_handoff`, `runtime_*`, `capability_matrix`,
|
||||
`provider_scenario`); five describe one renderer's output (`unity_*`,
|
||||
`electron_shell_projection`, `web_layout_probe`, `tui_grid`); seven describe the
|
||||
plugin system (`plugin*`). Every schema but `command_invocation`,
|
||||
`command_result`, `entity_soa_view`, `input_*` and `asset_catalog` sets
|
||||
`additionalProperties: true`.
|
||||
|
||||
**P2. Commands travel in four overlapping envelopes, and the result is hand-built
|
||||
JSON.** `command.v1` (descriptor, open), `command_invocation.v1` (closed),
|
||||
`command_receipt.v1` (open, `state` enum `accepted|denied|pending|reconciled`)
|
||||
and `command_result.v1` (closed: exactly `schema, receipt, transientProjection,
|
||||
pluginPayload, draftDirective`). Ghostlight `crates/ghostlight-dungeon/src/eve.rs`
|
||||
at `327cbe3`: `validate_invocation` (`:665-700`) re-checks the envelope by string
|
||||
comparison; `command_result` (`:734-`) takes `state: &str` and builds the receipt
|
||||
with `json!`; its own log filter names a state `"completed"` that the receipt
|
||||
enum does not admit (`:746`). Today it passes only `accepted` and `denied`
|
||||
(tally of call sites). The browser rejects a flat result whole through `ajv`
|
||||
(`web/package.json` depends on `ajv`; `eve.rs:723-731` comment). Nothing but
|
||||
convention stands between a provider and a result the viewer refuses.
|
||||
|
||||
**P3. One surface kind has two schemas and is stored as an untyped blob.**
|
||||
`surface.v1` declares `type: const "surface-state"`; `surface_state.v1` exists
|
||||
only as code: CultLib `packages/cultcache-ts/src/swarm-documents.ts:82-96`
|
||||
(members `providerId, title, version, updatedAt, surface: object`) and Odin
|
||||
`crates/odin-core/src/documents.rs` (`EveSurfaceStateRecord { surface: Value }`;
|
||||
advertisement and interface binding are `{ value: Value }`). The load-bearing
|
||||
shape is JSON inside CultCache.
|
||||
|
||||
**P4. `interface_binding.v1` is not a binding.** weksa
|
||||
`scripts/weksa-daemon.mjs:321-341` writes routes (`endpoints`, `routes`,
|
||||
`cultMeshAddress`) and the whole operator surface into it; its document type
|
||||
parses anything (`schema: { parse: (value) => value }`, `:418`). It is a third
|
||||
copy of the advertisement's routes and the surface.
|
||||
|
||||
**P5. The text DSL lets the renderer write state.** `web/eve-dsl.js:103-155`:
|
||||
`var`, `collection`, `derive` (`count`, `latest`) live in a renderer-local mesh,
|
||||
and `applyAction` appends or sets values there on a button press
|
||||
(`web/fixtures/reactive-composition.eve`: `button "Append Event" append
|
||||
swarm.events ...`).
|
||||
|
||||
**P6. A domain-looking kind is layout plus a server-side command.** Muninn
|
||||
`crates/sleipnir-daemon/src/main.rs:1374-1415`: `binding.target` is a labelled
|
||||
target with `x, y, anchorX, anchorY` over a controller image; "listening" is the
|
||||
daemon's `pending_learn` state. Absolute positioning (Taffy `position:
|
||||
absolute` with insets), a button and a bound label compose it.
|
||||
|
||||
**P7. Login needs a host effect, not a plugin payload.** Ghostlight
|
||||
`web/src/heimdall-access-adapter.ts:48-66`: a `pluginPayload` of schema
|
||||
`heimdall.auth_navigation_receipt.v1` makes the client call
|
||||
`window.location.assign(url)`; on return it reads the URL and resumes
|
||||
(`:82`, `:134`).
|
||||
|
||||
**P8. Two live viewers render surfaces.** Ghostlight's web client vendors
|
||||
`@gamecult/eve-browser-lowering` and `eve-contracts` from `vendor/eve`
|
||||
(`web/package.json`). gamecult-ops `scripts/idunn/idunn-deployment-targets.ps1:262-272`
|
||||
at `9aed01f`: `nightwing-eve-browser-reference`, `Repo = "Eve"`,
|
||||
`Status = "runtime-enforced"`, `Deploy = $null`. Odin aggregates and renders
|
||||
nothing.
|
||||
|
||||
**P9. CultCache-rs members are serde types.** CultLib
|
||||
`packages/cultcache-rs/src/lib.rs:5501-5520`: a `DatabaseEntry` whose member is a
|
||||
struct holding an enum round-trips through the MessagePack store. Whether the TS,
|
||||
C# and Python CultCache runtimes decode a recursive tagged union written by Rust
|
||||
with parity was not established in this pass. It is the first thing the
|
||||
contract cut proves (see the cut order).
|
||||
|
||||
**P10. The stale `.voidbot` lines are still on Eve main.** `a758787`:
|
||||
`.gitignore:28-30` and `scripts/stage-to-eve.ps1:24` (`--exclude=.voidbot`).
|
||||
`git grep -n '\.voidbot' -- .gitignore scripts` prints exactly those four lines;
|
||||
`Sources/EVEViewController.mm` mentions `voidbot.swarm` (no dot) and is not
|
||||
matched.
|
||||
|
||||
**P11. The campaign's repo list has room for the early cuts only.** Mind,
|
||||
`thing2:campaign:self`: repos `GameCult/Eve`, `GameCult/gamecult-ops`.
|
||||
`eureka-substrate:cut_spec:cut-campaign-repos.r1` (target-revision repo sets, up
|
||||
to 16) is in force with no report; both its questions are answered
|
||||
(`eureka-substrate:ruling:campaign-repo-set-home`, `:campaign-repo-removal`).
|
||||
|
||||
**P12. Rust CultCache writes a union as serde's external tag; the C# reference
|
||||
writes `[key, fields]`. They do not read each other.** Probe pass
|
||||
`imagination-thing2-contract-cut`, 2026-10-10, local scratch
|
||||
`imagination-thing2-contract-cut/{rs,cs,ts}` against CultLib `6bdc23c9`. One
|
||||
record type in all three runtimes: `id`, `version`, `root: Node` with `Node`
|
||||
one of `Box { id, children: [Node] }`, `Text { id, content }`,
|
||||
`Use { id, composition, slots: [Node] }`, and `outcomes: [Outcome]` with a unit
|
||||
arm. Rust (`#[derive(DatabaseEntry)]`, serde enums) writes the payload
|
||||
`94 af..31 07 81 a3"Box" 92 ...`: each arm a one-entry map keyed by the variant
|
||||
name, fields positional, a unit arm the bare string `"Denied"`. C# (MessagePack
|
||||
`[Union(n, ...)]`, the reference) writes `94 af..31 07 92 00 92 ...`: each arm
|
||||
`[key, [fields]]`, the unit arm `[2, []]`. C# reading the Rust store fails:
|
||||
`MessagePackSerializationException: Failed to deserialize ProbeSurface value.
|
||||
<- Unexpected msgpack code 129 (fixmap) encountered.` Rust reading the C# store
|
||||
fails: `failed to decode CultCache entry "probe.surface" ... a value does not
|
||||
fit the store format`. Each runtime round-trips its own store. The
|
||||
`cultcache-rs` derive handles structs only
|
||||
(`cultcache-rs-derive/src/lib.rs:51`); enum members go through `rmp_serde`.
|
||||
Owner: CultLib (`thing2:follow_up:cultcache-union-parity`).
|
||||
|
||||
**P13. TypeScript CultCache has no typed codec.** `cult-cache.ts:679-692`: with
|
||||
no `formatter`, a payload decodes to plain arrays and maps and `schema.parse`
|
||||
sees them raw; the interop test hand-writes a positional formatter per type
|
||||
(`test/cult-cache.test.ts:1263-1310`). In the probe, a hand-written TS codec
|
||||
for the union read the Rust store, wrote a store Rust read back equal, and
|
||||
re-encoded byte-identical to Rust's payload; against the C# store the same
|
||||
codec refused the `[key, fields]` arm (`node is not a tag map`). A TS reader
|
||||
matches whichever encoding its author hand-codes.
|
||||
|
||||
**P14. Nothing live builds from the six retired family repos.** `git grep` at
|
||||
`origin/HEAD` of Ghostlight `327cbe3`, Odin `8e1de66`, VoidBot `46d891b`,
|
||||
Heimdall `7904e17`, Muninn `1d0ea0b`, weksa `d16cff5`, Stonks `3f6e91c`, Vili
|
||||
`8773c15`, Gjallar `6a92976`, StreamPixels `8d40541`, CultLib, Idunn `6667f0f`,
|
||||
gamecult-site `8169ee5` and Ymir `f8c0e39` for `EveUnity|EveFlutter|EveElectron|
|
||||
EveTui|EvePlugins|EveConformance` outside `*.md`: no match. gamecult-ops `9aed01f`
|
||||
matches only `docs/repo-census-2026-09/synthesis.html`; Eve matches a local
|
||||
smoke script (`scripts/run-aetheria-conformance-consumer-smoke.ps1:35`, a local
|
||||
path) and a spec; parked Mimir names them in one configuration file. GitHub,
|
||||
2026-10-10: all six unarchived; active workflows only on EveFlutter and EveTui
|
||||
(`runtime.yml`) and EveConformance (`conformance.yml`); `GameCult/Thing` and
|
||||
`GameCult/AetheriaThing` do not exist; AetheriaEve is archived.
|
||||
|
||||
**P15. Ghostlight's deploy compares its Eve clone's origin by exact string.**
|
||||
gamecult-ops `9aed01f` `scripts/deploy-ghostlight-yggdrasil.sh:20`
|
||||
(`eve_origin=https://github.com/GameCult/Eve.git`) and `:871-876`
|
||||
(`require_idunn_repo` refuses "a foreign origin" when `git remote get-url
|
||||
origin` differs), called at `:883`; the binding template carries the same URL
|
||||
(`idunn/yggdrasil/bindings/ghostlight.toml.in:14`). Editing either URL without
|
||||
re-pointing Yggdrasil's Idunn-owned clone stops the next Ghostlight deploy;
|
||||
leaving both on the old name keeps working through GitHub's rename redirect.
|
||||
The `Repo = "Eve"` targets (`idunn-deployment-targets.ps1:223`, `:236`,
|
||||
`:262`) are Thing 1 targets (iPad, Android, Nightwing browser reference) paired
|
||||
with `LocalPath = "$projectsRoot\Eve"`.
|
||||
|
||||
**P16. gamecult-site carries a generated Thing 1 surface.**
|
||||
`scripts/generate-vn-eve-surface.mjs` writes
|
||||
`GameCult/static/interactive/gamecult-compound/gamecult-compound.eve-gui.json`
|
||||
(`gamecult.eve.*` ids), referenced by `GameCult/Pitch.md:15`
|
||||
(`data-eve-gui=`). The census (C) does not list the site as a consumer.
|
||||
|
||||
**P17. The contract's integer leaves are signed because TS and Python have no
|
||||
unsigned leaf; the C# reference has one.** CultLib `b3bd8dd5`:
|
||||
`src/GameCult.Caching/CultSchemaTypeNames.cs` names any CLR type (so `UInt32`,
|
||||
`UInt64` and `Byte` slots exist in the reference); `cultcache-ts`
|
||||
`src/codec.ts` exports only `int32` and `int64` integer codecs and
|
||||
`cultcache-py` `leaves.py:147-148` only `int32` and `int64`. A `u64` above
|
||||
2^63 has no TS or Python reading, so versions stay `Int64` and counts `Int32`,
|
||||
and the non-negative rule is the contract's, not the leaf's. `Rgba` keeps `u8`
|
||||
(the reference's `Byte`); the TS and Python packages mirror its 0-255 range
|
||||
from a refusal golden until CultLib gives them a byte leaf.
|
||||
|
||||
**P18. A domain newtype's own `Deserialize` holds its rule at every decode
|
||||
door, and leaves the bytes unchanged.** Probe `imagination-contract-rs-r2/`
|
||||
(throwaway commit `fbbdaf79` on `649844aa`, Yggdrasil, 2026-10-12): `Seq(i32)`
|
||||
refusing below 1 and `Version(i64)` refusing below 0, each `Serialize` the inner
|
||||
integer and `Deserialize` through `new`. A result payload with seq 0 or -7 is
|
||||
refused by `rmp_serde::from_slice` ("Seq: at least 1") and by
|
||||
`cultcache_rs::decode_payload` (opaque: "a value does not fit the store
|
||||
format"); seq 0 through serde's in-memory `IntoDeserializer` is refused the same
|
||||
way, so the rule holds for any serde format. With `CommandResult.seq: Seq` and
|
||||
both composition versions `Version` (an `Int32` slot widened to `Int64`),
|
||||
`goldens_match` still compares byte-identical, `surface.cc` included. The
|
||||
derive reads each slot with `seq.next_element::<FieldType>()`
|
||||
(`cultcache-rs-derive/src/lib.rs`, `slot_codec`, at `b3bd8dd5`), so a field's
|
||||
type is the one owner of its domain on both paths. `DatabaseEntry` requires
|
||||
`DeserializeOwned` (`cultcache-rs/src/lib.rs:162`), so the public
|
||||
`Deserialize` of every kind cannot be removed: it is the decode door.
|
||||
|
||||
**P19. Dotted provider ids are the Verse's norm; dotted local names are not
|
||||
needed.** `git grep` over Ghostlight, VoidBot, Odin, Heimdall, Muninn, Stonks
|
||||
and Eve (operator checkouts, 2026-10-12): provider ids `voidbot.swarm`,
|
||||
`odin.allseer`, `stonks.market`, `sleipnir.input-mirror.raven`, `heimdall`;
|
||||
Thing 1 surface ids repeat the provider as a prefix (`voidbot.swarm.surface`,
|
||||
`ghostlight.play` in `crates/ghostlight-dungeon/src/mesh.rs:37`). No
|
||||
`gamecult.thing.*` record exists outside the contract's tests, and Thing 1
|
||||
records are rebuilt, not read (`thing:ruling:operator-compat-rebuild`), so the
|
||||
Thing 2.0 key grammar strands nothing. Probe evidence of the collision itself:
|
||||
Soul's `soul-thing2-contract-rs/rust1.log` KEY lines.
|
||||
|
||||
**P20. In C#, the constructor MessagePack calls is the one door, and the decode
|
||||
recursion fits a default thread inside the bound.** Probe
|
||||
`imagination-contract-cs-ts/` (throwaway commit `e5030d3b` on CultLib
|
||||
`b3bd8dd5`, Yggdrasil, image dotnet sdk 10.0, MessagePack 3.1.7, Debug and
|
||||
Release, log `cs2.log`). A `[MessagePackObject]` class with get-only `[Key]`
|
||||
properties and one constructor taking the keys in slot order, throwing on
|
||||
`seq < 1`:
|
||||
- public constructor: `CultDocumentRegistry` accepts it; `result-pending.bin`
|
||||
decodes through `CultDocumentMessagePackSerialization` and re-encodes
|
||||
identical; `result-seq-zero.bin` is refused by the constructor through
|
||||
`DeserializeUntyped`, through `MessagePackSerializerOptions.Standard`, and
|
||||
through `System.Text.Json` (which calls the same constructor); no message
|
||||
holds the golden's canary.
|
||||
- private constructor with `[SerializationConstructor]` under
|
||||
`[MessagePackObject(AllowPrivate = true)]`: the same results, the registry
|
||||
accepts it, reflection finds no public constructor and no public setter, and
|
||||
`System.Text.Json` refuses the type (`NotSupportedException`).
|
||||
- internal constructor without `AllowPrivate`: the registry refuses the type
|
||||
("no constructor MessagePack can call"), and `CultDocumentRegistry.Shared`
|
||||
then refuses every type in the assembly, so one bad type breaks the package.
|
||||
- `outcome-unknown-arm.bin` is refused through `CultDocumentMessagePackSerialization`
|
||||
(`CultUnionGuard`: "Union ... has no arm for key 4"), but decodes with a null
|
||||
outcome through `MessagePackSerializerOptions.Standard`: the guard lives only
|
||||
in CultLib's options, so a package decodes only through them.
|
||||
- Stack: a Box-shaped recursive union (3 containers per level) reaches the
|
||||
256 bound at 84 levels. Decoding recurses per level: depth 10 decodes on a
|
||||
64 KiB thread, depth 84 needs 256 KiB in Debug and 128 KiB in Release, and an
|
||||
overflow aborts the process (rc 134). .NET's 1 MiB default thread holds the
|
||||
deepest admitted payload with at least 4x margin, so C# carries no part of
|
||||
`cultlib-gaps:follow_up:decode-stack-safety`.
|
||||
|
||||
**P21. In TypeScript, cultcache-ts runs a document's codec and schema on both
|
||||
doors, and its recursion fits the default stack.** Same probe commit, image
|
||||
kotlin (Node 24.21.0), log `ts2.log`. cultcache-ts at `b3bd8dd5` (0.14.0) has
|
||||
`slots`, `union`, `list`, `lazy`, `unit` and the 256 payload bound;
|
||||
`decodeDocumentPayload` runs the codec then `schema.parse`, and
|
||||
`encodeDocumentPayload` runs `schema.parse` then the codec
|
||||
(`src/document.ts`). A `seq` codec delegating to `int32` with a `>= 1` check,
|
||||
and a `CommandResult` class with a `#brand` private field, a private
|
||||
constructor, a codec wrapping its slot codec that mints an instance on decode,
|
||||
and a `schema.parse` that refuses anything without the brand:
|
||||
- `result-pending.bin` decodes to an instance and re-encodes identical;
|
||||
`result-seq-zero.bin` is refused ("seq (slot 1): Seq must be at least 1");
|
||||
`outcome-unknown-arm.bin` is refused ("Union Outcome: unknown arm key 4");
|
||||
`result-extra-slot.bin` reads as seq 1; no message holds the canary.
|
||||
- `structuredClone`, spread and `JSON.parse` of an instance lose the brand,
|
||||
and encoding any of them, or an object literal with seq 0, is refused; the
|
||||
factory with seq 0 throws.
|
||||
- `tsc` (strict, NodeNext, ESM importing the CommonJS cultcache-ts) passes
|
||||
with `@ts-expect-error` on an object literal and a spread typed
|
||||
`CommandResult`, on `new CommandResult(...)`, and on assigning `seq`;
|
||||
`structuredClone(result)` and `JSON.parse` still type as `CommandResult`, so
|
||||
the runtime brand check is the guard there.
|
||||
- Stack: the same chain reaches the bound at 84 levels. Depth 84 decodes with
|
||||
`--stack-size` 200 and the default 984; at 60 to 120 the codec throws (a
|
||||
`RangeError`, wrapped by the codec into its own refusal), never aborting.
|
||||
|
||||
**P22. Packaging around the two packages.** Eve `6ae2da0`: `.gitignore`
|
||||
ignores `packages/*` with one `!` pair per package (lines 2-16), so a new
|
||||
package directory is invisible to git until it gets its pair, and global
|
||||
`obj/` (line 17) is where the Thing 1 test project keeps its output
|
||||
(`BaseOutputPath obj\bin\`). `GameCult.Eve.Surface.csproj` reads CultLib from
|
||||
`CultLibRoot`, defaulting to the sibling checkout `..\..\..\CultLib`.
|
||||
`npm view @gamecult/cultcache-ts` answers 404: cultcache-ts is on no registry,
|
||||
and the consumers that use it (Heimdall, StreamPixels) take it as
|
||||
`file:vendor/CultLib/packages/cultcache-ts` from a submodule. No file outside
|
||||
`crates/` and `docs/` names `thing-contract` or `GameCult.Thing.Contract` at
|
||||
`6ae2da0` except `Cargo.lock`.
|
||||
|
||||
## The model page
|
||||
|
||||
Every persistent kind Thing 2.0 has. Ids follow rename table T: schema
|
||||
`gamecult.thing.<x>.v<n>`, mesh key `thing:<family>:<id>`. No row keeps a
|
||||
compatible eve id (`operator-compat-rebuild`).
|
||||
|
||||
| Kind | What names it | What happens to it over time | Who decides (one writer) |
|
||||
| --- | --- | --- | --- |
|
||||
| Thing Script document (a published surface) | schema `gamecult.thing.surface.v1`; key `thing:surface:<providerId>.<label>`, where `label` is one segment (non-empty, no `.`), so the key splits at its last dot and is injective while provider ids stay dotted (P19); `version` is an `Int64` at least 0 in the record (P17) | Republished whole when its structure changes, `version + 1`; core ignores a version at or below the one it holds. Live values never cause a republish (they arrive through bindings). Retired when the provider deletes the record. A contract revision is a new `v<n>`; stores holding the old one are deleted and rebuilt by their daemon | The publishing provider. Core and viewers only read |
|
||||
| Composition | schema `gamecult.thing.composition.v1`; key `thing:composition:<publisher>.<name>.v<n>`, `name` one segment (no `.`), `n` an `Int64` at least 0 | Immutable once published: a change is a new `v<n>` under a new key; references pin the version. The standard library (`thing.std.*`) ships compiled into core and is also published by the Thing repo's release | The publisher (a provider, or the Thing repo for `thing.std`). Core resolves and expands; nothing else writes |
|
||||
| Binding | not a document: a field of a node in a Thing Script document or composition (`Bind`) | Lives and dies with the document that holds it | Declared by the document's writer; resolved only by core |
|
||||
| Bound state record | the provider's own schema and key (e.g. `voidbot.swarm_control_state.v1`) | The provider's lifecycle | The provider. Core never writes typed state; a change goes through a command |
|
||||
| Viewer session state (focus, scroll offsets, editing drafts, selection, IME composition, open overlays) | the core instance; node ids within a resolved document | Not persisted and never published; dies with the session. A draft that must outlive the session is state, and state belongs to a provider | Thing core |
|
||||
| Command declaration | not a document: an entry of the Thing Script document (`CommandDecl`, id local to the document) | With its document | The document's writer |
|
||||
| Command invocation | schema `gamecult.thing.command_invocation.v1`; key `thing:invocation:<invocationId>` (a UUID minted by core per user action, also the idempotency key) | Written once, never edited; retained by the provider's command boundary until its final result, then the provider's retention | Thing core, on behalf of the viewer; the transport sets the caller identity |
|
||||
| Command result | schema `gamecult.thing.command_result.v1`; key `thing:result:<invocationId>.<seq>` (`seq` an `Int32` from 1, refused below 1 on construction and decode; the key is injective because `seq` is the last dot segment and never holds a dot; a pending result may be followed by a final one) | Written once per `seq`; the final outcome ends the invocation | The provider that handles the command, through the contract's constructors only; core validates on receipt |
|
||||
| Asset catalog | schema `gamecult.thing.asset_catalog.v1`; key `thing:asset_catalog:<providerId>.<catalogId>`, `catalogId` one segment (no `.`); `version` an `Int64` at least 0; asset width and height `Int32` at least 0 | Republished whole, `version + 1`; image nodes reference `catalogId` + asset id | The provider |
|
||||
| Theme | schema `gamecult.thing.theme.v1`; key `thing:theme:<themeId>` | Versioned like a document; documents reference tokens, never a theme | The host application that embeds Thing (the GameCult brand theme is one such document); `thing.std` default theme in core |
|
||||
| Provider discovery (today `gamecult.eve.provider_advertisement.v1`) | Not a Thing kind (`thing2:ruling:advertisement-owner`): owned by the Verse discovery owner and renamed once there (`thing2:follow_up:discovery-owner-campaign`) | Until its owner lands, the eve advertisement is read through core's discovery port, unchanged | The discovery owner; the provider writes, Odin aggregates |
|
||||
| Accessibility tree | not persisted: derived by core per frame from the resolved document and layout (AccessKit tree updates) | Rebuilt on every change | Thing core; backends bridge to the platform |
|
||||
| Render-command list | not persisted and never published: in-process from core to backend | Per frame | Thing core |
|
||||
| Geometry and render-command goldens | test fixtures in the Thing repo (`tests/goldens/`) | Regenerated only by a reviewed change to core; a backend never writes one | The Thing repo's core tests |
|
||||
|
||||
## The consumer contract
|
||||
|
||||
Consumers see four things (target, "The consumer contract"). Each is a set of
|
||||
types in the contract crate and its language packages; nothing in core is
|
||||
visible to them (`narrow-consumer-contract`).
|
||||
|
||||
### 1. Thing Script
|
||||
|
||||
**The node set is closed.** A document's node is exactly one of:
|
||||
|
||||
| Node | Carries | Covers today's kinds (census C§7) |
|
||||
| --- | --- | --- |
|
||||
| `Box` | layout style (Taffy: display flex, grid or block; direction, wrap, gap, padding, margin; size, min, max in px, %, fr, auto, content; position relative or absolute with insets; overflow visible, clip or scroll), paint (background, border, radius as theme tokens or literal colours), accessibility role from a closed set (group, region, list, list item, heading, dialog, toolbar), label, children | `pane`, `card`, `panel`, `dashboard`, `cockpit`, `rail`, `row`, `grid`, `list`, `partition`, `surface`, `form`, `interface`, `binding.target` layout |
|
||||
| `Text` | content (literal or `Bind`), text style token, wrap, max lines, selectable | `text`, `stat`, label parts of `metric` |
|
||||
| `Image` | asset reference (catalog + id), fit, alt text | `image.*`, `avatar` |
|
||||
| `Button` | content children, `on_activate: CommandRef`, enabled (literal or `Bind`) | `control.button`, `command` (Vili) |
|
||||
| `Toggle` | checked (`Bind`), `on_change: CommandRef` | `control.toggle` |
|
||||
| `Slider` | min, max, step, value (`Bind`), commit command | `control.slider`, `control.range` |
|
||||
| `TextInput` | single or multi-line, value kind text or number, placeholder, initial value (`Bind`), submit command | `control.input.text`, `control.input.textarea`, `control.input.number`, `input.number` |
|
||||
| `Select` | options (literal or a bound collection), value (`Bind`), command | `input.select`, `control.select`, `control.choice-group` |
|
||||
| `Repeat` | a bound collection and a template subtree instantiated per item, keyed by item key; template bindings may read item members | `list ... bind <collection>` (BD5) |
|
||||
| `Overlay` | anchored to a node or the viewport, modal or not, dismiss command | `modal`, `control.popup`, tooltips |
|
||||
| `Plot` | a bound numeric series, closed marks (line, bar, area), axis ranges | `graph`; sparklines a dashboard consumer expects |
|
||||
| `Embed` | a host slot kind (string the host declares it supports) and slot id; laid out as a leaf of a given size; draws one opaque host command | `embed.norn`, `embed.tex`, `world.scene*`, `field.*`, `canvas.editor`, game viewports |
|
||||
| `Use` | a composition reference (`publisher.name.v<n>`) and typed arguments | `metric`, `inspector.kv`, `control.tabs`, `heimdall.access_gate`, `vn.stage`, every domain kind |
|
||||
|
||||
Every node has an id (unique within its document after expansion), `visible`
|
||||
(literal or `Bind`) and an optional accessibility label. A domain need is met by
|
||||
a composition (`compose-not-kind`); a need no composition can meet is an `Embed`
|
||||
slot drawn by the host, never a new node.
|
||||
|
||||
**Compositions are documents, resolved by core.** A composition declares typed
|
||||
parameters (text, number, bool, `Bind`, `CommandRef`, child slots) and a body of
|
||||
nodes that reference them. Core expands `Use` by substitution only: there is no
|
||||
expression language. Compositions never own commands; a composition that needs
|
||||
an action takes a `CommandRef` parameter, and the command is handled by the
|
||||
publisher of the document that uses it. `thing.std` provides the common ones
|
||||
(`metric`, `card`, `pane`, `dashboard`, `rail`, `key-value`, `stat`), so no
|
||||
consumer language re-implements them.
|
||||
|
||||
**Text form.** Whether Thing Script also has a text syntax beside the typed
|
||||
document, and which is the authority, is `thing2:question:thing-script-form`.
|
||||
The node set, compositions and rules here hold under every option.
|
||||
|
||||
### 2. Binding (BD1-BD9 collapse to one path)
|
||||
|
||||
`Bind { source: StateRef, member: MemberPath, format: Option<Format> }`, where
|
||||
`StateRef` is `{ provider: Option<ProviderId>, schema: SchemaId, key: RecordKey }`
|
||||
(absent provider = the document's publisher) or `Item` inside a `Repeat`
|
||||
template. A bound collection is `{ provider, schema, key_prefix }` (the records of
|
||||
one type) or a list member of one record. `Format` is closed: number (precision,
|
||||
percent, unit), duration, timestamp, prefix and suffix text.
|
||||
|
||||
| Shape | Fate |
|
||||
| --- | --- |
|
||||
| BD1 `stateBindings` | Survives as `Bind`; `routeKind`, `routeDescription` and `sourceId` die (core routes through discovery, a document names no transport) |
|
||||
| BD2 local drafts | Die as bindings: a draft is core's editing state of `TextInput`, `Slider` or `Select` |
|
||||
| BD3 `writeCommand`, `captureBindings`, `draftDirective` | Fold into commands: a `CommandDecl` lists the input nodes it captures; a completed result says whether captured drafts clear |
|
||||
| BD4 world-root props | Leave the contract: an `Embed` slot whose host owns the world view |
|
||||
| BD5 `var`, `collection`, `derive`, `bind` | `bind` and `collection` survive as `Bind` and `Repeat`; renderer-local `var` and `derive` die (P5): derived values are published by the state's owner |
|
||||
| BD6 whole-document republish | Survives for structure only (`version + 1`); per-tick republish of values dies |
|
||||
| BD7 `interface_binding` | Dies (P4): routes are discovery's, the surface is the document |
|
||||
| BD8 embedded documents | `Embed` |
|
||||
| BD9 reload and poll | Dies: core subscribes through the CultMesh port |
|
||||
|
||||
### 3. Commands (one envelope)
|
||||
|
||||
- `CommandDecl { id, payload_schema: SchemaId, captures: [NodeId], confirm: Option<Text> }`
|
||||
in the Thing Script document. A node may reference only a declared command.
|
||||
- `CommandInvocation { invocation_id, surface: (key, version), command, payload, captured: [(NodeId, Value)], client, issued_at, delegation }`.
|
||||
`payload` is a typed record of `payload_schema`. The caller's identity is set by
|
||||
the transport session and has no field a consumer can write.
|
||||
- `CommandResult { invocation_id, seq, outcome }`, with `outcome` one of
|
||||
`Pending { message }`, `Completed { source_version, clear_drafts, effect }`,
|
||||
`Denied { reason, node }` and `Failed { reason }`. `effect` is closed:
|
||||
`OpenSurface(key)` or `OpenExternal { url, resume: CommandRef }` (P7). The
|
||||
receipt is the result: `command.v1`, `command_receipt.v1`,
|
||||
`transientProjection` and `pluginPayload` have no successor.
|
||||
- Handlers in every language receive a decoded invocation and return an
|
||||
`Outcome`; the language package writes the result. No package takes a state
|
||||
as a string or converts a map into a result. Decoding is the one other door
|
||||
(every CultCache kind is deserializable, P18): it holds the same domain as the
|
||||
constructors because each field's type owns its rule. A decoded result can
|
||||
name any invocation; binding a result to an invocation core issued is core's
|
||||
validation (`thing:ruling:thing2-one-solver-taffy`), not the contract's.
|
||||
|
||||
### 4. Advertisement
|
||||
|
||||
What Thing needs from discovery is only "this provider exists and is reachable
|
||||
here": its surfaces are its `gamecult.thing.surface` records and its commands are
|
||||
declared inside them. Discovery leaves Thing (`thing2:ruling:advertisement-owner`);
|
||||
the contract has no advertisement type, and core reads discovery through a port.
|
||||
|
||||
### Rules that must die under their own mutation
|
||||
|
||||
Each is a test or negative check in the cut that lands it; deleting the rule's
|
||||
code fails it.
|
||||
|
||||
1. A result decodes only into `CommandResult`; a record with an extra or missing
|
||||
field is refused at decode in every language package (golden fixture with an
|
||||
extra field).
|
||||
2. Core turns a result that fails to decode, or one for an unknown
|
||||
`invocation_id`, into a visible `Malformed` state for that invocation and a
|
||||
log line; no invocation stays pending past its timeout without one.
|
||||
3. `outcome` is an enum in every package; no API takes a state as a string.
|
||||
4. A document with an unknown node kind, an undeclared `CommandRef`, or a
|
||||
duplicate node id after expansion is refused whole; nothing falls back along
|
||||
a kind path.
|
||||
5. A `Use` that does not resolve renders a visible placeholder with a diagnostic;
|
||||
it is never dropped silently.
|
||||
6. A `Bind` whose record has a different schema, or whose member is missing,
|
||||
renders a typed binding error; no value is coerced.
|
||||
7. Core holds no API that writes typed state; its only outbound write is a
|
||||
`CommandInvocation` (negative check over the core crate).
|
||||
8. No backend crate or package depends on `taffy` or `parley` (negative check on
|
||||
the dependency tree): backends receive positioned commands and shaped glyph
|
||||
runs.
|
||||
9. A lower or equal `version` of a document is ignored.
|
||||
10. Geometry and render-command goldens for the fixture set are byte-identical
|
||||
across runs; a backend's parity test consumes the core's list and asserts it
|
||||
drew every command it was given.
|
||||
|
||||
### Boundaries
|
||||
|
||||
- Consumers depend on the contract packages only. Core, backends and the
|
||||
standard library are internal.
|
||||
- Core reaches CultMesh, the clock, fonts and the platform through narrow ports
|
||||
(traits), so a parser, resolver, layout or command test runs without a daemon.
|
||||
- Backends draw and bridge: input events in, render commands and the
|
||||
accessibility tree out. IME uses the platform channel the ruling names (hidden
|
||||
textarea on the web).
|
||||
|
||||
## Thing 1 kinds and their fate
|
||||
|
||||
| Thing 1 id (`gamecult.eve.*`) | Fate |
|
||||
| --- | --- |
|
||||
| `surface.v1`, `surface_state.v1` (code only) | Collapse into `gamecult.thing.surface.v1` (P3) |
|
||||
| `command.v1`, `command_invocation.v1`, `command_receipt.v1`, `command_result.v1`, `operation_payload.v1` | Collapse into `command_invocation.v1` and `command_result.v1` (P2) |
|
||||
| `local_draft.v1`, `interface_binding.v1` | Die (BD2, P4) |
|
||||
| `asset_catalog.v1` | Survives as `gamecult.thing.asset_catalog.v1` |
|
||||
| `provider_advertisement.v1` | Per `thing2:question:advertisement-owner` |
|
||||
| `plugin.v1`, `plugin_advertisement.v1`, `plugin_abi.request.v1`, `plugin_abi.response.v1`, `plugin_abi_fixture.v1`, `plugin_receipt.v1`, `plugin_witness.v1`, `plugin_handoff.v1` | Die: domain plugins become compositions or `Embed` slots |
|
||||
| `conformance_*`, `capability_matrix.v1`, `provider_scenario.v1`, `provider_handoff.v1`, `runtime_*`, `local_provider_catalog.v1` | Die: parity is geometry goldens; discovery is not a fixture catalog |
|
||||
| `unity_*`, `electron_shell_projection.v1`, `web_layout_probe.v1`, `tui_grid.v1` | Die: renderer outputs are not contract kinds |
|
||||
| `entity_soa_view.v1`, `input_bindings.v1`, `input_capability.v1` | Die (no carrier outside the kernel, B25) |
|
||||
| `mimir.eve_*`, `cultmesh.eve_surface.v0` | Mimir's own ids, renamed by Mimir's port when it is revived (Mimir is archived) |
|
||||
|
||||
## The authority map
|
||||
|
||||
- **Owner of geometry, text, focus, editing, accessibility, binding resolution
|
||||
and result validation:** Thing core (`one-solver`, `core-owns-text`,
|
||||
`live-state-core`).
|
||||
- **Owner of a surface's content and of its commands' outcomes:** the provider
|
||||
that publishes the document.
|
||||
- **Owner of the contract types:** the contract crate; each language package is
|
||||
held to the crate's golden wire fixtures and owns nothing of its own.
|
||||
- **Derived:** the resolved tree, layout, render-command list, accessibility
|
||||
tree and goldens. Viewer session state is core-owned and never published.
|
||||
- **Demoted:** the browser is no longer the layout oracle; renderers are no
|
||||
longer owners of kinds, drafts, pointer resolution or result validation; the
|
||||
text DSL's local mesh is no longer a writer of state; `surface_state`,
|
||||
`interface_binding` and the receipt are no longer documents.
|
||||
- **Forbidden writers:** a backend computing layout or shaping text; a provider
|
||||
writing a result through anything but the package constructor; core writing
|
||||
typed state; a document naming a transport; any compatible eve id; a new node
|
||||
kind added for one domain.
|
||||
- **Shared paths:** every viewer (web shell, Ghostlight's client, TUI, future
|
||||
hosts) embeds the same core; every consumer language publishes through a
|
||||
package held to the same goldens.
|
||||
- **Deletion line:** Thing 1's tree in the kernel repo, its six family repos and
|
||||
every `gamecult.eve.*` reader and writer in the ported consumers. Each port
|
||||
deletes its consumer's Thing 1 path in the same cut.
|
||||
|
||||
## Cut order (deliverables)
|
||||
|
||||
0. **`stale-voidbot-cleanup`** (Eve main, any time): spec
|
||||
`thing2:cut_spec:cut-stale-voidbot-cleanup.r1`. Owned here by
|
||||
`thing:follow_up:stale-voidbot-cleanup-on-main`.
|
||||
1. **Contract** (code home per `thing2:question:code-home`): the contract crate
|
||||
with the types above, published schemas `schemas/gamecult.thing.*`, golden
|
||||
wire fixtures, and the TypeScript and C# packages held to them (the live
|
||||
consumers' languages: Rust, JS/TS, C#). The first proof (P9) was run: a
|
||||
Rust-written recursive union does not decode in the C# reference, nor the
|
||||
reverse (P12), and TS decodes only what a hand-written codec says (P13). The
|
||||
contract waited on CultLib's union parity
|
||||
(`thing2:follow_up:cultcache-union-parity`), which is on CultLib main by
|
||||
`b3bd8dd5` (P20, P21) and unreleased. Three cuts: `contract-rs`, then
|
||||
`contract-ts` and `contract-cs`. The published schema of each kind is its
|
||||
CultCache catalog entry, identical in the three packages; no JSON Schema
|
||||
files are written, because Thing 2.0 has no JSON reader of these kinds (the
|
||||
browser reads them through core, not `ajv`). Python and Kotlin packages
|
||||
follow as their own deliverables.
|
||||
`contract-rs` landed on Eve main (`6ae2da0`) after two Soul passes
|
||||
(`thing2:verdict:cut-contract-rs.s1`, `.s2`). The siblings are mapped
|
||||
against `6ae2da0` and run side by side, touching only their own package and
|
||||
one `.gitignore` insertion each, at separate anchors:
|
||||
`thing2:cut_spec:cut-contract-cs.r2` and `thing2:cut_spec:cut-contract-ts.r2`.
|
||||
Waiting, one line each:
|
||||
- Cross-runtime store check, after both siblings land: the C# package opens
|
||||
a store the TS package wrote and the reverse, which proves the catalog
|
||||
type names (`GameCult.Thing.Contract.<RustName>`) agree; neither sibling
|
||||
can prove it alone.
|
||||
- Publication: when `cultlib-gaps:cut_spec:cut-union-parity-release.r3`
|
||||
publishes cultcache-ts, the TS package's dependency moves from the sibling
|
||||
`file:` path to the published version, and both packages get a release
|
||||
line. Until then both read CultLib from the sibling checkout at the Cargo
|
||||
rev (P22).
|
||||
- CultLib pin bump: when `cultlib-gaps:follow_up:decode-stack-safety` lands,
|
||||
move the pin and run `deep_tree` on the default thread (its 16 MiB thread
|
||||
goes); the crate docs' hazard note goes with it.
|
||||
- Missing-slot golden: re-added only when
|
||||
`cultlib-gaps:follow_up:rust-missing-slot-vs-reference` settles the rule.
|
||||
2. **Core**, in three cuts: (a) resolution, compositions with `thing.std`,
|
||||
binding through the CultMesh port, Taffy layout, render commands, the command
|
||||
dispatcher and result validation, goldens; (a)'s decode of Verse surfaces
|
||||
waits on `cultlib-gaps:follow_up:decode-stack-safety`; (b) Parley text, focus, editing and
|
||||
IME state; (c) the AccessKit tree.
|
||||
3. **TUI backend** (Rust): cell measurement in the Taffy measure function,
|
||||
terminal drawing, keyboard input. First, because it exercises the whole
|
||||
pipeline with no platform bridge and gives agents a reader.
|
||||
4. **Web backend and shell**: core to wasm, a thin JS host (canvas drawing,
|
||||
hidden-textarea IME, DOM accessibility mirror), the discovery client, and the
|
||||
web shell that replaces the Nightwing browser reference.
|
||||
5. **Consumer ports**, one cut per live consumer, each carrying its rename and
|
||||
deleting its Thing 1 path (`consumer-port-carries-rename`). Cutover shape per
|
||||
`thing2:question:port-cutover`. Proposed order: Odin (reads Thing 2 kinds),
|
||||
VoidBot (richest binding, one command), Ghostlight with Heimdall (text inputs,
|
||||
the auth effect, the flat-result case; two cuts, Heimdall's access gate
|
||||
becomes a composition), Muninn and sleipnir, weksa, Stonks, Vili, Gjallar,
|
||||
StreamPixels, then gamecult-ops (the Nightwing target), then CultLib (its eve
|
||||
document definitions in `cultcache-ts` and `cultmesh-kotlin` are deleted, B26).
|
||||
These need more repos than the campaign lists: `thing2:question:campaign-repo-limit`.
|
||||
6. **Retirement**: delete Thing 1's tree from the kernel repo. The renames,
|
||||
archives and site prose are re-specified as `thing:cut_spec:cut-repo-renames.r2`
|
||||
and `cut-site-prose.r2` and do not wait for it (P14).
|
||||
|
||||
**Repos this map retires:** EveUnity, EveFlutter, EveElectron, EveTui, EvePlugins
|
||||
and EveConformance (no Thing 2.0 role: no domain kinds, no per-renderer
|
||||
lowerers, parity by goldens). They are archived under their Eve names
|
||||
(`thing2:ruling:retired-repo-names`) by `thing:cut_spec:cut-repo-renames.r2`,
|
||||
ahead of retirement, because nothing live builds from them (P14). The kernel
|
||||
repo is renamed Thing in the same cut (`thing2:ruling:code-home`); its URLs in
|
||||
gamecult-ops stay on the redirect until the port that owns each one (P15).
|
||||
AetheriaEve stays under its rulings.
|
||||
|
||||
**Parked (dormant or blocked) consumers:** Bifrost, repixelizer, Mimir, Hermodr,
|
||||
Loki, Sai, AquaSynth, Brokkr, Ymir. Their Thing 1 pins keep resolving at the
|
||||
archived repos; each ports when it is next revived. Unity and Flutter backends
|
||||
have no live consumer and are parked until one appears.
|
||||
|
||||
## Rationale
|
||||
|
||||
- **Why the contract before core.** Every later cut, and every consumer port,
|
||||
depends on the types; the cross-runtime decode of a recursive union (P9) is the
|
||||
riskiest unknown and is cheapest to find before core exists.
|
||||
- **Why TUI before web.** It has no platform bridge, so the first end-to-end run
|
||||
tests core, not a host; the web backend then adds only drawing, IME and the
|
||||
accessibility mirror.
|
||||
- **Why compositions are resolved by core, not expanded by each language's
|
||||
builder.** Builder-side expansion would put `metric`, `card` and every shared
|
||||
composition in each consumer language, which is Thing 1's per-renderer lowerer
|
||||
problem moved to the publishers.
|
||||
- **Why no expression language in documents.** P5 shows where it leads: the
|
||||
renderer becomes a second owner of derived state. Owners publish derived
|
||||
values; `Format` covers presentation.
|
||||
- **Why Odin ports first.** It is the aggregator every viewer discovers
|
||||
through; under either cutover option it must read Thing 2 kinds before a
|
||||
provider publishes them.
|
||||
- **Why the result has no free-form payload.** P2 and P7: the only live uses of
|
||||
`pluginPayload` and `transientProjection` were a navigation and a status,
|
||||
which are `effect` and `outcome`.
|
||||
- **Why each integer's rule lives in its field type.** The decode door cannot be
|
||||
closed (P18), so a rule held only by a constructor is a rule the wire skips.
|
||||
A newtype per domain (`Seq`, `Version`, `Count`, `Segment`) is read by both
|
||||
paths, keeps the bytes, and gives the TS and C# packages one refusal golden
|
||||
each to mirror. This is serde's `try_from` pattern, written by hand because
|
||||
the CultCache derive emits the codec itself.
|
||||
- **Why the key grammar constrains only the local segment.** Provider ids and
|
||||
publishers are owned by discovery (`thing2:ruling:advertisement-owner`) and
|
||||
are dotted in the Verse today (P19); the contract owns only `label`, `name`
|
||||
and `catalogId`. Refusing the separator there makes every key split at its
|
||||
last dots, the way DNS labels and the mind's own `<root>:<kind>:<local>` keys
|
||||
stay unambiguous. A different separator between provider and label would
|
||||
instead impose a grammar on ids the contract does not own. Listing a
|
||||
provider's records filters on the record's provider field, not a key prefix,
|
||||
because a dotted provider is a prefix of a longer one.
|
||||
- **Why each sibling holds its domains where its decoder constructs.** The
|
||||
rule from contract-rs is that a domain is checked on every door by one owner.
|
||||
C# has no newtype that keeps the leaf's bytes without a custom formatter,
|
||||
which CultLib owns, but MessagePack, System.Text.Json and code all build a
|
||||
C# value through the one constructor that takes the slots (P20), so that
|
||||
constructor, calling `ThingDomain`, is the door. cultcache-ts runs each
|
||||
slot's codec on encode and decode (P21), so a domain codec over the plain
|
||||
leaf is the door there, the same shape as Rust's newtype `Deserialize`.
|
||||
`CommandResult` is closed differently in each language: a private
|
||||
constructor under `AllowPrivate` in C#, a `#brand` field checked by the
|
||||
document's `schema.parse` in TS, because TypeScript's structural types let
|
||||
any object with the right fields pass as a class with only public members.
|
||||
- **Why the siblings read CultLib from a sibling checkout, not a submodule.**
|
||||
The C# package already reads CultLib that way (`CultLibRoot`), cultcache-ts
|
||||
is on no registry yet (P22), and the Rust crate pins the same commit by
|
||||
Cargo rev. A submodule would be a third pin to keep equal to the Cargo rev;
|
||||
publication replaces the `file:` path instead.
|
||||
- **Why contract-rs merges before CultLib's stack fix.** The overflow is the
|
||||
decode door's (`cultlib-gaps:follow_up:decode-stack-safety`) and every Rust
|
||||
reader of these kinds inherits it whether or not the contract merges. The
|
||||
contract decodes no hostile input; core's Verse decode does, so that is where
|
||||
the wait sits (cut 2a). The crate docs name the hazard meanwhile.
|
||||
69
docs/thing2-target.md
Normal file
69
docs/thing2-target.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Thing 2.0 target
|
||||
|
||||
Campaign `thing2` in the eureka mind. This page holds the rationale and the
|
||||
design truths the rebuild produces. The typed `thing2:target` holds the
|
||||
invariants, the not-in-scope list and the canonical implementations; the
|
||||
rulings are in the mind and are cited here by id, never restated.
|
||||
|
||||
## Why
|
||||
|
||||
Thing 1 handed layout to each host's own engine (CSS flexbox in the browser,
|
||||
Flutter widgets, Unity UI Toolkit's Yoga), so there were as many layout specs
|
||||
as renderers and the browser was the oracle in practice. Its contract grew
|
||||
domain kinds (`inventory.grid`, `vn.stage`, 41 kinds in the fixtures) that every
|
||||
lowerer in five languages had to implement. Parity was screenshots and gap
|
||||
ledgers, not geometry. Live state binding, pointer resolution and command
|
||||
results lived in each renderer, which is where the July failures came from
|
||||
(views that had to reload, failing commands, schema-rejected results). Evidence:
|
||||
`docs/research/thing2-foundation-prior-art.md`.
|
||||
|
||||
Operator, 2026-10-10: "More Things, fewer things."
|
||||
|
||||
- **More Things:** every surface is a composition of Things, published as Thing
|
||||
Script. An inventory grid, a VN stage or an Odin wall is a composition, not a
|
||||
contract kind.
|
||||
- **Fewer things:** a small closed primitive set, one layout solver, one closed
|
||||
render-command list, thin backends, far fewer repos.
|
||||
|
||||
## Shape
|
||||
|
||||
```
|
||||
typed retained document (Thing Script, bound to typed CultMesh state)
|
||||
-> Thing core: Taffy layout, Parley text, focus, editing/IME state, AccessKit tree
|
||||
-> closed render-command list (+ accessibility tree)
|
||||
-> thin backends: draw, and bridge input/IME/accessibility to the platform
|
||||
```
|
||||
|
||||
Rulings in force: `thing:ruling:thing2-one-solver-taffy`,
|
||||
`thing:ruling:thing2-thing-owns-text`, `thing:ruling:wire-rename-freeze-fold`,
|
||||
`thing:ruling:operator-compat-rebuild`, `thing:ruling:hold-renames-for-thing2`.
|
||||
Text, IME and accessibility evidence:
|
||||
`docs/research/thing2-text-input-a11y-prior-art.md`.
|
||||
|
||||
## The consumer contract
|
||||
|
||||
Consumers (provider daemons, Verse daemons, CultLib runtimes, game hosts) never
|
||||
see Taffy, Parley or render commands. They depend on four things only:
|
||||
|
||||
1. **Thing Script** — the typed documents they publish: primitives plus shared
|
||||
compositions.
|
||||
2. **Binding** — how a surface points at typed CultMesh state and receives
|
||||
live updates.
|
||||
3. **Commands** — how input returns as a typed command with a schema-checked
|
||||
result.
|
||||
4. **Advertisement** — how a provider announces itself to Odin.
|
||||
|
||||
Each waiting consumer migrates by one port cut onto this contract, which also
|
||||
carries its rename to `gamecult.thing.*` (rename table T and edge table E1-E7 in
|
||||
`docs/thing-campaign.md`). The consumer census sizes the floor of Thing Script;
|
||||
Thing is a general library, so capability a reasonable consumer would expect is
|
||||
not cut for lack of a current caller.
|
||||
|
||||
## Open before any cut is mapped
|
||||
|
||||
- The identity, lifecycle and authority table for every persistent kind
|
||||
(model page in the prose map).
|
||||
- The consumer census: what each waiting consumer publishes, binds and commands
|
||||
today.
|
||||
- Where Thing 2.0's code lives (a fork, to be put with prior art).
|
||||
- Which backend lands first for cutover: at minimum web, plus TUI for agents.
|
||||
|
|
@ -21,7 +21,6 @@ tar `
|
|||
--exclude=flutter `
|
||||
--exclude=tools `
|
||||
--exclude=web `
|
||||
--exclude=.voidbot `
|
||||
--exclude=packages `
|
||||
-cf $archive `
|
||||
-C $projectRoot .
|
||||
|
|
|
|||
Loading…
Reference in a new issue