diff --git a/Cargo.lock b/Cargo.lock index 96fbfee..e8fbf15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,12 @@ dependencies = [ "object", ] +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "async-trait" version = "0.1.92" @@ -222,6 +228,30 @@ 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" @@ -256,6 +286,12 @@ 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" @@ -437,7 +473,7 @@ dependencies = [ "cultcache-rs 0.2.0", "ed25519-dalek", "hmac", - "rand", + "rand 0.9.5", "reed-solomon-erasure", "rmp-serde", "rmpv", @@ -463,7 +499,7 @@ dependencies = [ "cultcache-rs 0.3.1", "ed25519-dalek", "hmac", - "rand", + "rand 0.9.5", "rmp-serde", "rmpv", "serde", @@ -647,7 +683,6 @@ dependencies = [ "schemars", "serde", "serde_json", - "serde_path_to_error", "tempfile", "tokio", ] @@ -981,6 +1016,16 @@ 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" @@ -1145,6 +1190,7 @@ dependencies = [ "proc-macro2", "quote", "rmp-serde", + "rust_decimal", "rustix", "serde", "serde_json", @@ -1157,10 +1203,11 @@ dependencies = [ [[package]] name = "model-connector" version = "0.1.0" -source = "git+https://eureka.gamecult.org/forge/GameCult/ModelConnector.git?rev=c1802d85bee3ae04b0c5737563c8343c1fef515d#c1802d85bee3ae04b0c5737563c8343c1fef515d" +source = "git+https://eureka.gamecult.org/forge/GameCult/ModelConnector.git?rev=480cf941c8b56ff34cd641fdcccbb72bcdce80c6#480cf941c8b56ff34cd641fdcccbb72bcdce80c6" dependencies = [ "aes-gcm", "base64", + "cultcache-rs 0.3.1", "cultnet-rs 0.2.0", "rand_core 0.6.4", "rmp-serde", @@ -1382,6 +1429,15 @@ 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" @@ -1422,16 +1478,37 @@ 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", + "rand_chacha 0.9.0", "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" @@ -1630,6 +1707,23 @@ 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" @@ -2009,6 +2103,36 @@ 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" @@ -2163,6 +2287,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -2371,6 +2496,15 @@ 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 9fea14c..aec5f07 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,7 @@ 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`; `--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 + `--ollama-url` and `--embedding-model`. 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 diff --git a/crates/eureka-pipeline/golden/envelopes.txt b/crates/eureka-pipeline/golden/envelopes.txt index 596a7cf..cbd66ac 100644 --- a/crates/eureka-pipeline/golden/envelopes.txt +++ b/crates/eureka-pipeline/golden/envelopes.txt @@ -25,10 +25,6 @@ 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 eab3ca8..9288d4c 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -96,12 +96,6 @@ impl Absent for Option { } } -impl Absent for bool { - fn absent(&self) -> bool { - !*self - } -} - impl Absent for Vec { fn absent(&self) -> bool { self.is_empty() @@ -118,7 +112,7 @@ macro_rules! unbounded_scalars { )*}; } -unbounded_scalars!(u16, u32, u64, bool); +unbounded_scalars!(u32, u64, bool); fn bound(field: &str, limit: usize, actual: usize) -> Result<(), PipelineRefusal> { if actual > limit { @@ -578,99 +572,6 @@ 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 } @@ -680,7 +581,6 @@ unit_enums! { FindingOrigin { Introduced, PreExisting } RunOperator { Operator, Mind } RunTurn { PersonaTurn, SelfRun } - MemorySort { Asked, Decided, Committed, Learned, Did, Idea, Rapport } } value_types! { @@ -810,51 +710,6 @@ 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: