diff --git a/Cargo.lock b/Cargo.lock index 04c05d1..45d9d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,10 +143,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] -name = "arrayvec" -version = "0.7.8" +name = "ar_archive_writer" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] [[package]] name = "async-trait" @@ -219,30 +222,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" -[[package]] -name = "borsh" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.5", -] - [[package]] name = "bumpalo" version = "3.20.3" @@ -277,12 +256,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - [[package]] name = "chrono" version = "0.4.45" @@ -401,7 +374,7 @@ source = "git+https://github.com/GameCult/CultLib.git?rev=8fc74c709444b884a2bd33 dependencies = [ "anyhow", "chrono", - "cultcache-rs-derive", + "cultcache-rs-derive 0.1.0", "fs2", "redb", "rmp-serde", @@ -412,6 +385,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cultcache-rs" +version = "0.3.1" +source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23" +dependencies = [ + "anyhow", + "chrono", + "cultcache-rs-derive 0.2.0", + "fs2", + "redb", + "rmp-serde", + "serde", + "serde_bytes", + "serde_json", + "serde_stacker", + "stacker", + "uuid", + "windows-sys 0.59.0", +] + [[package]] name = "cultcache-rs-derive" version = "0.1.0" @@ -422,6 +415,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "cultcache-rs-derive" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "cultnet-rs" version = "0.1.0" @@ -431,10 +434,10 @@ dependencies = [ "anyhow", "base64", "chrono", - "cultcache-rs", + "cultcache-rs 0.2.0", "ed25519-dalek", "hmac", - "rand 0.9.5", + "rand", "reed-solomon-erasure", "rmp-serde", "rmpv", @@ -448,6 +451,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cultnet-rs" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=a4eae9056152335662f1115f97bf35fb00ab6d23#a4eae9056152335662f1115f97bf35fb00ab6d23" +dependencies = [ + "aes-gcm", + "anyhow", + "base64", + "chrono", + "cultcache-rs 0.3.1", + "ed25519-dalek", + "hmac", + "rand", + "rmp-serde", + "rmpv", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "socket2", + "uuid", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -597,7 +625,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "cultcache-rs", + "cultcache-rs 0.2.0", "rmp-serde", "schemars", "serde", @@ -611,7 +639,7 @@ dependencies = [ "anyhow", "chrono", "clap", - "cultnet-rs", + "cultnet-rs 0.1.0", "huginn-daemon", "huginn-mind", "rmcp", @@ -891,7 +919,7 @@ dependencies = [ "anyhow", "base64", "chrono", - "cultnet-rs", + "cultnet-rs 0.1.0", "huginn-mind", "rmp-serde", "serde", @@ -909,8 +937,8 @@ dependencies = [ "anyhow", "base64", "chrono", - "cultcache-rs", - "cultnet-rs", + "cultcache-rs 0.2.0", + "cultnet-rs 0.1.0", "eureka-pipeline", "jsonschema", "rmp-serde", @@ -952,16 +980,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "indexmap" -version = "2.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", -] - [[package]] name = "inout" version = "0.1.4" @@ -1116,18 +1134,43 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "cultcache-rs", - "cultnet-rs", + "cultcache-rs 0.2.0", + "cultnet-rs 0.1.0", "eureka-pipeline", "eureka-state", "huginn-mind", "libc", + "model-connector", "proc-macro2", "quote", - "rust_decimal", + "rmp-serde", + "rustix", "serde", + "serde_json", + "sha2", "syn 2.0.119", "tempfile", + "uuid", +] + +[[package]] +name = "model-connector" +version = "0.1.0" +source = "git+https://eureka.gamecult.org/forge/GameCult/ModelConnector.git?rev=c1802d85bee3ae04b0c5737563c8343c1fef515d#c1802d85bee3ae04b0c5737563c8343c1fef515d" +dependencies = [ + "aes-gcm", + "base64", + "cultnet-rs 0.2.0", + "rand_core 0.6.4", + "rmp-serde", + "rmpv", + "schemars", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "thiserror", + "zeroize", ] [[package]] @@ -1208,6 +1251,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1329,15 +1381,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1347,6 +1390,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.47" @@ -1368,37 +1421,16 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -1597,23 +1629,6 @@ dependencies = [ "serde_bytes", ] -[[package]] -name = "rust_decimal" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.8", - "rand 0.9.5", - "serde", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "rustc_version" version = "0.4.1" @@ -1755,6 +1770,23 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_stacker" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" +dependencies = [ + "serde", + "serde_core", + "stacker", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -1839,6 +1871,19 @@ dependencies = [ "der", ] +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1963,36 +2008,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml_datetime" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.17+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" -dependencies = [ - "winnow", -] - [[package]] name = "tracing" version = "0.1.44" @@ -2097,6 +2112,7 @@ checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", + "sha1_smol", "wasm-bindgen", ] @@ -2146,7 +2162,6 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", - "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -2355,15 +2370,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/README.md b/README.md index 028f0b2..9fea14c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ steer its next action, and the provenance of every admitted claim. Upstream: `https://github.com/GameCult/Huginn.git` +**Status.** Live: `huginn-mind` (admission with receipts, typed queries, derived status), `huginn-daemon` (CultNet RUDP with a Qdrant semantic index), and the `huginn` CLI from `crates/eureka-state` (whoami, view, query, admit, schema). The `eureka` instance runs on Yggdrasil. Entry points: `cargo check --workspace`, `crates/huginn-mind/src/admission.rs`, `crates/huginn-daemon/src/serve.rs`, `schemas/cultnet/`. Planned: what each crate does next is owned by the `eureka-body` cut specs in the mind and `docs/eureka-body-map.md`; the legacy `.voidbot/` Persona migration belongs to the portable-Persona work. + ## Authority - An instance owns its mind; Huginn owns the state. Huginn is the single @@ -24,7 +26,9 @@ Upstream: `https://github.com/GameCult/Huginn.git` restart compares the mind with the collection and writes what is missing. An unreachable Qdrant or Ollama shows in `whoami` as the index's status and is retried; admission never waits on it. The daemon requires `--qdrant-url`, - `--ollama-url` and `--embedding-model`. A `query` carrying `semantic` is + `--ollama-url` and `--embedding-model`; `--ollama-url` repeats for each GPU + host and `--reserve-ollama-url` for each CPU host, and the GPU hosts embed + together while the CPU hosts serve only when none can. A `query` carrying `semantic` is answered through the same selection as any other: the index supplies candidate ids and scores, and the mind keeps only the documents it holds and the selection admits (ask for `in_force` to exclude resolved ones), ordered by @@ -38,7 +42,7 @@ edits `.cc` state. Huginn reads and writes minds. ## Layout -A Cargo workspace of three crates: +A Cargo workspace of five crates (`crates/eureka-pipeline` holds the document shapes and keys, `crates/mind-body` the control store, launch primitive and `mind-heartbeat` timeline; the three below are the mind, the daemon and the client): - `crates/huginn-mind`: storage, identity, admission, and queries and derived status over memory documents. diff --git a/crates/eureka-pipeline/README.md b/crates/eureka-pipeline/README.md index 5d9c633..25fa726 100644 --- a/crates/eureka-pipeline/README.md +++ b/crates/eureka-pipeline/README.md @@ -18,13 +18,6 @@ and every encoding-relevant variant against `golden/envelopes.txt`. A diff there is a wire change; do not regenerate it to make the test pass. An additive field adds golden lines and never changes an existing one. -Opt-in, against a real store: `stored_documents_read_back` decodes, validates -and keys every stored pipeline document and asserts that it re-encodes to the -stored key, type and payload bytes. Take a copy of the store (the test copies -the file again before opening it, so the copy may be read-only), never open the -live one: - -```sh -sudo cp /var/lib/gamecult/huginn/minds/eureka/mind.redb "$SNAP/minds/eureka/mind.redb" -HUGINN_MIND_SNAPSHOT="$SNAP" cargo test -p eureka-pipeline -- --ignored --exact tests::stored_documents_read_back --nocapture -``` +The real-store gate is huginn-mind's `#[ignore]` `query::tests::stored_documents_read_back`, +which checks these stored bytes and the read path together (the leaf cannot see the read +path); the Huginn Upgrade runbook in gamecult-ops runs it. diff --git a/crates/eureka-pipeline/golden/envelopes.txt b/crates/eureka-pipeline/golden/envelopes.txt index cbd66ac..596a7cf 100644 --- a/crates/eureka-pipeline/golden/envelopes.txt +++ b/crates/eureka-pipeline/golden/envelopes.txt @@ -25,6 +25,10 @@ sample.instance yggdrasil:instance:self epiphany.pipeline.instance.v2 EpiphanyPi sample.stewardship yggdrasil:stewardship:gamecult_-epiphany.n1 epiphany.pipeline.stewardship.v2 EpiphanyPipelineStewardshipDocument 9185a8696e7374616e6365a979676764726173696ca47265706fb147616d6543756c742f4570697068616e79a873657175656e636501ab61737369676e65645f6f6eaa323032362d30392d3135a46e6f7465b954686520457572656b612063616d706169676e207265706f2e sample.hand_off yggdrasil:hand_off:thought-cage.gamecult_-epiphany.2026-09-15 epiphany.pipeline.hand_off.v2 EpiphanyPipelineHandOffDocument 9186ad66726f6d5f696e7374616e6365a979676764726173696cab746f5f696e7374616e6365ac74686f756768742d63616765a47265706fb147616d6543756c742f4570697068616e79a9646f63756d656e747392ac657572656b612d7374617465b6657572656b612d73746174653a72756c696e673a5238a6726561736f6ed92854686520776f726b73746174696f6e206d696e642074616b6573207468652063616d706169676e2ea968616e6465645f6f6eaa323032362d30392d3135 sample.run yggdrasil:run:mind-2026-10-09-1 epiphany.pipeline.run.v2 EpiphanyPipelineRunDocument 9189a8696e7374616e6365a979676764726173696ca56c6162656cb16d696e642d323032362d31302d30392d31ab6f706572617465645f6279a44d696e64a47475726ea753656c6652756ea4686f7374a979676764726173696caa737461727465645f6f6eaa323032362d30392d3135aa6275646765745f757364a3372e35a6636c61696d739182a46b696e64a743757453706563a26964d925657572656b612d73746174653a6375745f737065633a6375742d72756e2d6b696e642e7232a963616d706169676e7391ac657572656b612d7374617465 +sample.pulse yggdrasil:pulse:2026-10-31t041200-t0 epiphany.pipeline.pulse.v2 EpiphanyPipelinePulseDocument 9185a8696e7374616e6365a979676764726173696ca56c6162656cb4323032362d31302d3331743034313230302d7430b174656d706f5f766f74655f626561745f73cd0258b0757267656e63795f7065726d696c6c65ccfaa46e6f7465d92254686520737761726d2069732069646c653b20612073686f7274657220626561742e +sample.memory yggdrasil:memory:2026-10-31t041200-t0-m0 epiphany.pipeline.memory.v2 EpiphanyPipelineMemoryDocument 9189a8696e7374616e6365a979676764726173696ca56c6162656cb7323032362d31302d3331743034313230302d74302d6d30a4736f7274a9436f6d6d6974746564a474657874d936492074616b6520746865206261636b67726f756e6420736861646572733b204c6962627920686f6c64732074686520736f756e64732ea561626f757491ab406c696262793a63686174a66f726967696e81a4526f6f6d82a5726f6f6d73a5737761726daa68656172645f66726f6d91ab406c696262793a63686174a7736f757263657391a3246531b173616c69656e63655f7065726d696c6c65cd02bcac636f6e736f6c69646174657390 +sample.research eureka-state:research:p1.scope epiphany.pipeline.research.v2 EpiphanyPipelineResearchDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca573636f7065a5636c61696dd93654686520656d626564206c696d6974206973203430393620746f6b656e7320776974682073696c656e74207472756e636174696f6e2ea664657461696cd934526561642066726f6d20746865206461656d6f6e277320696e646578206d6f64756c6520616e642070726f626564206c6976652ea8696e666572726564c2a7736f75726365739481a45265706f82a47265706fb147616d6543756c742f4570697068616e79a2617484a470617468d9256372617465732f687567696e6e2d6461656d6f6e2f7372632f696e6465782f6d6f642e7273aa73746172745f6c696e6532a8656e645f6c696e6538a6636f6d6d6974a8356639383232386481a357656283a375726cd92268747470733a2f2f7a657474656c6b617374656e2e64652f61746f6d69636974792fa4776f726bb65a657474656c6b617374656e2061746f6d6963697479ac7265747269657665645f6f6eaa323032362d30392d313581a84f6273657276656481a865766964656e636583a46b696e64a454657374a76c6f6361746f72aa636172676f2074657374a6726573756c74a26f6b81a8446f63756d656e7481a97265666572656e636582a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a5238a67365727665739182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab6f627365727665645f6f6eaa323032362d30392d3135 +sample.idea eureka-state:idea:p1.i1 epiphany.pipeline.idea.v2 EpiphanyPipelineIdeaDocument 9189a863616d706169676eac657572656b612d7374617465a470617373a27031a56c6162656ca26931a57469746c65b24f6e65206e6f74652070657220636c61696da469646561d9204b65657020612070617373206173206d616e7920736d616c6c206e6f7465732ea974726164656f666673b74d6f726520646f63756d656e747320746f206c6973742ea872657374735f6f6e9182a46b696e64a85265736561726368a26964be657572656b612d73746174653a72657365617263683a70312e73636f7065a6746f776172649182a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131ab70726f706f7365645f6f6eaa323032362d30392d3135 resolution.superseded eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581aa5375706572736564656481a262799282a46b696e64a652756c696e67a26964b6657572656b612d73746174653a72756c696e673a523882a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 resolution.fixed-by-none eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a26279c0a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 resolution.fixed-by-some eureka-state:resolution:question.Q1.n1 epiphany.pipeline.resolution.v2 EpiphanyPipelineResolutionDocument 9185a77375626a65637482a46b696e64a85175657374696f6ea26964b8657572656b612d73746174653a7175657374696f6e3a5131a873657175656e636501a76f7574636f6d6581a5466978656482a6636f6d6d6974a83566393832323864a2627982a46b696e64a94375745265706f7274a26964d921657572656b612d73746174653a6375745f7265706f72743a6375742d33612e6831a9726174696f6e616c65a852756c656420412eab7265736f6c7665645f6f6eaa323032362d30392d3135 diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index d19d192..eab3ca8 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -96,6 +96,12 @@ impl Absent for Option { } } +impl Absent for bool { + fn absent(&self) -> bool { + !*self + } +} + impl Absent for Vec { fn absent(&self) -> bool { self.is_empty() @@ -112,7 +118,7 @@ macro_rules! unbounded_scalars { )*}; } -unbounded_scalars!(u32, u64, bool); +unbounded_scalars!(u16, u32, u64, bool); fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> { if actual > limit { @@ -572,6 +578,99 @@ fn run_is_well_formed(run: &PipelineRun, at: &str) -> Result<(), PipelineRefusal } } +/// The longest and shortest beat a Mind may vote for, in seconds. +pub const TEMPO_VOTE_MIN_S: u32 = 60; +pub const TEMPO_VOTE_MAX_S: u32 = 86_400; +/// Urgency is in thousandths: 0 declares none, 1000 the most. +pub const URGENCY_MAX_PERMILLE: u16 = 1000; + +/// A pulse votes a tempo or declares an urgency, in bounds, or says nothing it +/// could be read for: an empty pulse is refused. +fn pulse_is_well_formed(pulse: &PipelinePulse, at: &str) -> Result<(), PipelineRefusal> { + if let Some(beat) = pulse.tempo_vote_beat_s + && !(TEMPO_VOTE_MIN_S..=TEMPO_VOTE_MAX_S).contains(&beat) + { + return Err(format_error(&format!("{at}.tempo_vote_beat_s"), &beat.to_string())); + } + if let Some(urgency) = pulse.urgency_permille + && urgency > URGENCY_MAX_PERMILLE + { + return Err(format_error(&format!("{at}.urgency_permille"), &urgency.to_string())); + } + if pulse.tempo_vote_withdrawn && pulse.tempo_vote_beat_s.is_some() { + return Err(format_error(&format!("{at}.tempo_vote_withdrawn"), "a pulse that votes a beat and withdraws the vote")); + } + if pulse.tempo_vote_beat_s.is_none() && !pulse.tempo_vote_withdrawn && pulse.urgency_permille.is_none() { + return Err(format_error(&format!("{at}.tempo_vote_beat_s"), "a pulse that votes and declares nothing")); + } + Ok(()) +} + +/// The most thousandths a memory's salience may carry. +pub const SALIENCE_MAX_PERMILLE: u16 = 1000; + +/// A memory says something (its text is not empty), its salience is in +/// thousandths, and it consolidates only memories. +fn memory_is_well_formed(memory: &PipelineMemory, at: &str) -> Result<(), PipelineRefusal> { + if memory.text.0.trim().is_empty() { + return Err(format_error(&format!("{at}.text"), "an empty memory")); + } + if let MemoryOrigin::Direct { with, .. } = &memory.origin + && with.is_empty() + { + return Err(format_error(&format!("{at}.origin.with"), "a direct memory that names no one she heard it from")); + } + if memory.salience_permille > SALIENCE_MAX_PERMILLE { + return Err(format_error(&format!("{at}.salience_permille"), &memory.salience_permille.to_string())); + } + for (index, entry) in memory.consolidates.iter().enumerate() { + if entry.kind != PipelineKind::Memory { + return Err(format_error(&format!("{at}.consolidates[{index}].kind"), entry.kind.name())); + } + } + Ok(()) +} + +/// A research note states a claim and stands on at least one source that +/// names something: a Repo source a file and a range that does not end before +/// it starts, a Web source its address and work, an Observed one its +/// locator and result, a Document one a note other than this one. A claim +/// with no source is not research. An inferred note names what it inferred +/// from in those sources. Two notes citing each other cannot be seen from one +/// document; that refusal would need the image, so it is not made here. +fn research_is_well_formed(research: &PipelineResearch, at: &str) -> Result<(), PipelineRefusal> { + if research.claim.0.trim().is_empty() { + return Err(format_error(&format!("{at}.claim"), "an empty claim")); + } + if research.sources.is_empty() { + return Err(format_error(&format!("{at}.sources"), "a research note that names no source")); + } + let own_key = format!("{}:research:{}.{}", research.campaign.0, research.pass.0, research.label.0); + let blank = |text: &str| text.trim().is_empty(); + for (index, source) in research.sources.iter().enumerate() { + let (field, empty) = match source { + ResearchSource::Repo { at, .. } if blank(&at.path.0) => ("at.path", "a repo source pinning no file"), + ResearchSource::Repo { at, .. } if at.end_line < at.start_line => ("at.end_line", "a repo source whose range ends before it starts"), + ResearchSource::Web { url, .. } if blank(&url.0) => ("url", "a web source with no address"), + ResearchSource::Web { work, .. } if blank(&work.0) => ("work", "a web source naming no work"), + ResearchSource::Observed { evidence } if blank(&evidence.locator.0) => ("evidence.locator", "an observation naming nothing observed"), + ResearchSource::Observed { evidence } if blank(&evidence.result.0) => ("evidence.result", "an observation with no result"), + ResearchSource::Document { reference } if reference.kind == PipelineKind::Research && reference.id.0 == own_key => ("reference", "a note that is its own source"), + _ => continue, + }; + return Err(format_error(&format!("{at}.sources[{index}].{field}"), empty)); + } + Ok(()) +} + +/// An idea says something. It may serve no fork and rest on nothing. +fn idea_is_well_formed(idea: &PipelineIdea, at: &str) -> Result<(), PipelineRefusal> { + if idea.idea.0.trim().is_empty() { + return Err(format_error(&format!("{at}.idea"), "an empty idea")); + } + Ok(()) +} + unit_enums! { EvidenceKind { Command, Test, Mutation, Probe, SourceRead, Capture } ClaimOutcome { Holds, Falsified, Unproven } @@ -581,6 +680,7 @@ unit_enums! { FindingOrigin { Introduced, PreExisting } RunOperator { Operator, Mind } RunTurn { PersonaTurn, SelfRun } + MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport } } value_types! { @@ -710,6 +810,51 @@ value_types! { instance: Slug, label: Label, operated_by: RunOperator, turn: RunTurn, host: Short, started_on: Date, budget_usd: Short, claims: Vec[64], campaigns: Vec[8], } => run_is_well_formed + /// One Mind's heartbeat vote, in her own mind: the beat she wants (the swarm's + /// tempo is the lower median of every Mind's newest vote), the withdrawal of + /// her standing vote (`tempo_vote_withdrawn`, an explicit null: she counts as + /// silent again), and the urgency she declares for herself until her next + /// Persona turn. A pulse says at least one of the three; a withdrawal and a + /// beat are never both. Instance-rooted: the key is `:pulse: