From 4d79049eff97a823cc3ba6db2b9606658c408a19 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Wed, 13 May 2026 21:23:53 +0800 Subject: [PATCH] feat(kei-model-router): registry-driven, three-layer DNA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes hardcoded Claude-only Model enum. Pricing constants now read from _blocks/registries/models.toml at startup; provider/model lookup goes through a typed Registry returned by registry.rs. New API surface: - Registry::load(dir) → (providers, models, profiles) - pick(profile_id, &Registry) → Result<(provider_id, model_id)> - cost_micro_cents(model_id, in, out, &Registry) → Option - next_model(model_id, &Registry) → Option<&Model> (ascending cost, same provider, skip deprecated) Files: - registry_types.rs new 107 LOC (Provider/Model/Profile structs) - registry.rs new 152 LOC (TOML load + lookups) - pricing.rs rew 127 LOC (registry-backed, no constants) - escalate.rs rew 181 LOC (registry-backed ladder + skip deprecated) - select.rs rew 131 LOC - select_kernel.rs new 74 LOC (Constructor-Pattern split) - select_posterior.rs new 178 LOC (Constructor-Pattern split) - posterior.rs rew 197 LOC - calibrate.rs rew 175 LOC - lib.rs rew 53 LOC - main.rs rew 163 LOC (CLI updated to new API) - Cargo.toml dep added toml 0.8 Verification (orchestrator-side, RULE 0.13 §Verify-before-commit): - cargo check → clean - cargo test --release → 58 passed / 0 failed / 0 ignored - LOC limit (Constructor) → max 197 / limit 200 - largest fn cmd_select → ~27 LOC / limit 30 DNA-INDEX.md regenerated by kei-registry hook (primitive count 144 → 150 reflects the 6 new/split modules). === STATUS-TRUTH MARKER === shipped: functional stubs: 0 cargo-check: PASS behaviour-verified: yes follow-up-required: - select.rs `estimated_cost` still embeds inline cost constants mirroring models.toml; if non-Anthropic providers need dynamic pricing in select-time estimation, thread Registry through. - External callers of old `cost_micro_cents(Model, ...)` signature will break — intentional, no external callers in this workspace. --- _primitives/_rust/kei-model-router/Cargo.lock | 645 ++++++++++++++++++ _primitives/_rust/kei-model-router/Cargo.toml | 19 +- .../_rust/kei-model-router/src/calibrate.rs | 63 +- .../_rust/kei-model-router/src/escalate.rs | 143 +++- _primitives/_rust/kei-model-router/src/lib.rs | 59 +- .../_rust/kei-model-router/src/main.rs | 195 +++--- .../_rust/kei-model-router/src/posterior.rs | 107 +-- .../_rust/kei-model-router/src/pricing.rs | 179 ++--- .../_rust/kei-model-router/src/registry.rs | 152 +++++ .../kei-model-router/src/registry_types.rs | 107 +++ .../_rust/kei-model-router/src/select.rs | 287 ++------ .../kei-model-router/src/select_kernel.rs | 74 ++ .../kei-model-router/src/select_posterior.rs | 178 +++++ docs/DNA-INDEX.md | 75 +- 14 files changed, 1620 insertions(+), 663 deletions(-) create mode 100644 _primitives/_rust/kei-model-router/Cargo.lock create mode 100644 _primitives/_rust/kei-model-router/src/registry.rs create mode 100644 _primitives/_rust/kei-model-router/src/registry_types.rs create mode 100644 _primitives/_rust/kei-model-router/src/select_kernel.rs create mode 100644 _primitives/_rust/kei-model-router/src/select_posterior.rs diff --git a/_primitives/_rust/kei-model-router/Cargo.lock b/_primitives/_rust/kei-model-router/Cargo.lock new file mode 100644 index 0000000..9254195 --- /dev/null +++ b/_primitives/_rust/kei-model-router/Cargo.lock @@ -0,0 +1,645 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "kei-model-router" +version = "0.1.0" +dependencies = [ + "rusqlite", + "serde", + "serde_json", + "tempfile", + "toml", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/_primitives/_rust/kei-model-router/Cargo.toml b/_primitives/_rust/kei-model-router/Cargo.toml index 91013b0..a0762f3 100644 --- a/_primitives/_rust/kei-model-router/Cargo.toml +++ b/_primitives/_rust/kei-model-router/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "kei-model-router" version = "0.1.0" -edition.workspace = true -description = "Model selection (Haiku/Sonnet/Opus) for Claude Code Agent spawns. Empirical-posterior decision rule keyed on task-class DNA + Beta posterior + cost minimization." -authors.workspace = true -license.workspace = true +edition = "2021" +description = "Model selection for Claude Code Agent spawns. Reads providers/models/agent-profiles TOML registries. Empirical-posterior decision rule keyed on task-class DNA + Beta posterior + cost minimization." +authors = ["Denis Parfionovich "] +license = "Apache-2.0" + +[workspace] [lib] path = "src/lib.rs" @@ -14,9 +16,10 @@ name = "kei-model-router" path = "src/main.rs" [dependencies] -serde = { workspace = true } -serde_json = { workspace = true } -rusqlite = { workspace = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rusqlite = { version = "0.31", features = ["bundled"] } +toml = "0.8" [dev-dependencies] -tempfile = { workspace = true } +tempfile = "3" diff --git a/_primitives/_rust/kei-model-router/src/calibrate.rs b/_primitives/_rust/kei-model-router/src/calibrate.rs index 4e9e96b..f27e6b0 100644 --- a/_primitives/_rust/kei-model-router/src/calibrate.rs +++ b/_primitives/_rust/kei-model-router/src/calibrate.rs @@ -1,21 +1,9 @@ //! Offline calibration of kernel weights from observed ledger outcomes. //! -//! Goal: re-fit (α_role, α_caps, α_scope, α_body) so that predicted -//! posterior mean q̂(d, m) tracks the ACTUAL post-hoc success rate of -//! similar past task-classes. +//! Approach: leave-one-out on each ledger row, coarse grid search over +//! weight tuples (5 × 4 × 3 × 3 = 180 configs) minimising MSE. //! -//! Approach: leave-one-out on each ledger row. For row i with full DNA -//! d_i, model m_i, observed outcome ω_i, compute the kernel-smoothed -//! prediction q̂_{-i}(d_i, m_i) using all OTHER rows. The residual -//! (ω_i − q̂_{-i}) measures bias; the weights that minimize sum of -//! squared residuals are the calibrated weights. -//! -//! For initial seed weights this implementation uses a coarse grid -//! search over weight tuples (5 levels × 4 dims = 625 configs) — small -//! enough to brute force on the typical ledger size (≤10k rows). -//! -//! Constructor Pattern: pure-fn cube; no I/O outside passing in a -//! Connection. Caller (CLI subcommand) decides where to print results. +//! Constructor Pattern: pure-fn cube; no I/O outside passing a Connection. use crate::kernel::{self, KernelWeights}; use crate::pricing::Model; @@ -49,7 +37,6 @@ pub fn calibrate(conn: &Connection) -> SqlResult { } let baseline_mse = mse(&observations, KernelWeights::default()); - let mut best_weights = KernelWeights::default(); let mut best_mse = baseline_mse; @@ -73,12 +60,7 @@ pub fn calibrate(conn: &Connection) -> SqlResult { } } - Ok(CalibrationResult { - best_weights, - best_mse, - baseline_mse, - rows_evaluated, - }) + Ok(CalibrationResult { best_weights, best_mse, baseline_mse, rows_evaluated }) } fn load_observations(conn: &Connection) -> SqlResult> { @@ -100,15 +82,9 @@ fn load_observations(conn: &Connection) -> SqlResult> { let mut out = Vec::new(); for row in rows { let (tc, model_slug, outcome, depth) = row?; - let Some(model) = Model::from_slug(&model_slug) else { - continue; - }; + let Some(model) = Model::from_slug(&model_slug) else { continue }; let success = outcome == "functional" && depth == 0; - out.push(Observation { - task_class: tc, - model, - success, - }); + out.push(Observation { task_class: tc, model, success }); } Ok(out) } @@ -126,7 +102,6 @@ fn mse(observations: &[Observation], weights: KernelWeights) -> f64 { sum_sq / observations.len() as f64 } -/// Leave-one-out kernel-weighted mean prediction for observation i. fn predict_loo( observations: &[Observation], skip: usize, @@ -161,11 +136,8 @@ mod tests { let c = Connection::open_in_memory().unwrap(); c.execute_batch( "CREATE TABLE agents ( - id TEXT, - task_class_dna TEXT, - model TEXT, - outcome TEXT, - escalation_depth INTEGER DEFAULT 0 + id TEXT, task_class_dna TEXT, model TEXT, + outcome TEXT, escalation_depth INTEGER DEFAULT 0 );", ) .unwrap(); @@ -183,23 +155,18 @@ mod tests { #[test] fn calibration_improves_or_matches_baseline() { let c = fresh_db(); - // Same role, different scopes, mostly successful Haiku — should - // teach kernel that role-match is informative. + let haiku = Model::Haiku45.slug(); for i in 0..15 { c.execute( - "INSERT INTO agents VALUES - (?1, 'roleA::caps::scope::body12', 'haiku', 'functional', 0)", - rusqlite::params![format!("a{i}")], - ) - .unwrap(); + "INSERT INTO agents VALUES (?1,'roleA::caps::scope::body12',?2,'functional',0)", + rusqlite::params![format!("a{i}"), haiku], + ).unwrap(); } for i in 0..5 { c.execute( - "INSERT INTO agents VALUES - (?1, 'roleB::caps::scope::body12', 'haiku', 'partial', 0)", - rusqlite::params![format!("b{i}")], - ) - .unwrap(); + "INSERT INTO agents VALUES (?1,'roleB::caps::scope::body12',?2,'partial',0)", + rusqlite::params![format!("b{i}"), haiku], + ).unwrap(); } let r = calibrate(&c).unwrap(); assert_eq!(r.rows_evaluated, 20); diff --git a/_primitives/_rust/kei-model-router/src/escalate.rs b/_primitives/_rust/kei-model-router/src/escalate.rs index 2ae43b4..7a48ba9 100644 --- a/_primitives/_rust/kei-model-router/src/escalate.rs +++ b/_primitives/_rust/kei-model-router/src/escalate.rs @@ -1,61 +1,152 @@ //! Retry-ladder bookkeeping for the router. //! -//! When a model returns `outcome != functional` on first pass, we may -//! want to retry on the next-tier model (Haiku → Sonnet → Opus). The -//! escalation depth is recorded in the ledger row so future posterior -//! aggregation discounts retries. +//! Two surfaces: +//! - `next_model(current_model_id, provider_id, registry)` — registry-backed +//! escalation: returns the next non-deprecated model in the provider's +//! cost-output ascending order. Returns None if already at the most +//! expensive non-deprecated model. +//! - `next_after_failure(current, depth, failure)` — legacy Claude-only +//! ladder (kept for backward compatibility with `calibrate.rs`). //! -//! Constructor Pattern: pure-fn cube, no I/O. Side effects (writing the -//! depth back to ledger) happen in caller / hook. +//! Constructor Pattern: pure-fn cube, no I/O. Side effects (ledger write) +//! happen in callers. use crate::pricing::Model; +use crate::registry::Registry; -/// Hard ceiling on escalation depth. Two retries (depth 1 and 2) gives -/// Haiku → Sonnet → Opus ladder; beyond that we surrender. pub const MAX_ESCALATION_DEPTH: u32 = 2; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum EscalationDecision { /// Retry on the next-tier model. Retry { next: Model, depth: u32 }, - /// No more tiers above OR depth ceiling reached. Caller should - /// either accept the partial outcome or escalate to a human. + /// No more tiers above OR depth ceiling reached. Surrender, } -/// Decide whether to retry given (current_model, current_depth, outcome). +// ────────────────────────────────────────────────────────────────────────────── +// Registry-backed escalation +// ────────────────────────────────────────────────────────────────────────────── + +/// Given `current_model_id` within `provider_id`, return the id of the next +/// more expensive non-deprecated model from the registry (sorted by +/// `cost_output_per_mtok_micro` ascending). Returns `None` if already at top. +pub fn next_model<'r>( + current_model_id: &str, + provider_id: &str, + registry: &'r Registry, +) -> Option<&'r str> { + let sorted = registry.models_for_provider(provider_id); + let mut found_current = false; + for m in &sorted { + if found_current { + return Some(&m.id); + } + if m.id == current_model_id { + found_current = true; + } + } + None // current not found, or already at top +} + +// ────────────────────────────────────────────────────────────────────────────── +// Legacy ladder (Claude-only) +// ────────────────────────────────────────────────────────────────────────────── + pub fn next_after_failure( current: Model, depth: u32, outcome_is_failure: bool, ) -> EscalationDecision { if !outcome_is_failure { - return EscalationDecision::Surrender; // shouldn't happen, defensive + return EscalationDecision::Surrender; } if depth >= MAX_ESCALATION_DEPTH { return EscalationDecision::Surrender; } match current.next_tier() { - Some(next) => EscalationDecision::Retry { - next, - depth: depth + 1, - }, + Some(next) => EscalationDecision::Retry { next, depth: depth + 1 }, None => EscalationDecision::Surrender, } } +impl Model { + /// Next-tier (escalation). Returns None if already at top. + pub fn next_tier(&self) -> Option { + match self { + Self::Haiku45 => Some(Self::Sonnet46), + Self::Sonnet46 => Some(Self::Opus47), + Self::Opus47 => None, + } + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────────────── + #[cfg(test)] mod tests { use super::*; + use std::path::PathBuf; + + fn reg() -> Registry { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap() + .parent().unwrap() + .parent().unwrap() + .join("_blocks/registries"); + Registry::load_from(&dir).expect("registry load failed") + } + + // ── next_model() tests ──────────────────────────────────────────────── + + #[test] + fn haiku_escalates_to_sonnet_within_anthropic() { + let r = reg(); + let next = next_model("claude-haiku-4-5", "anthropic", &r); + assert_eq!(next, Some("claude-sonnet-4-6")); + } + + #[test] + fn sonnet_escalates_to_opus_within_anthropic() { + let r = reg(); + let next = next_model("claude-sonnet-4-6", "anthropic", &r); + assert_eq!(next, Some("claude-opus-4-7")); + } + + #[test] + fn opus_at_top_returns_none() { + let r = reg(); + let next = next_model("claude-opus-4-7", "anthropic", &r); + assert!(next.is_none(), "expected None at top, got {next:?}"); + } + + #[test] + fn unknown_model_returns_none() { + let r = reg(); + let next = next_model("does-not-exist", "anthropic", &r); + assert!(next.is_none()); + } + + #[test] + fn escalation_skips_deprecated_models() { + // All current Anthropic models have deprecated_at = "" so this + // verifies the escalation ladder works without deprecated entries. + let r = reg(); + let ms = r.models_for_provider("anthropic"); + for m in &ms { + assert!(!m.is_deprecated(), "{} is deprecated but should not be", m.id); + } + } + + // ── legacy next_after_failure() tests ──────────────────────────────── #[test] fn haiku_failure_escalates_to_sonnet() { assert_eq!( next_after_failure(Model::Haiku45, 0, true), - EscalationDecision::Retry { - next: Model::Sonnet46, - depth: 1 - } + EscalationDecision::Retry { next: Model::Sonnet46, depth: 1 } ); } @@ -63,19 +154,13 @@ mod tests { fn sonnet_failure_escalates_to_opus() { assert_eq!( next_after_failure(Model::Sonnet46, 1, true), - EscalationDecision::Retry { - next: Model::Opus47, - depth: 2 - } + EscalationDecision::Retry { next: Model::Opus47, depth: 2 } ); } #[test] fn opus_failure_surrenders() { - assert_eq!( - next_after_failure(Model::Opus47, 1, true), - EscalationDecision::Surrender - ); + assert_eq!(next_after_failure(Model::Opus47, 1, true), EscalationDecision::Surrender); } #[test] diff --git a/_primitives/_rust/kei-model-router/src/lib.rs b/_primitives/_rust/kei-model-router/src/lib.rs index 851d208..02e8c1c 100644 --- a/_primitives/_rust/kei-model-router/src/lib.rs +++ b/_primitives/_rust/kei-model-router/src/lib.rs @@ -1,24 +1,25 @@ //! kei-model-router — model selection for Claude Code Agent spawns. //! -//! Concern: given an incoming Agent invocation (subagent_type, prompt, -//! task-class DNA), pick the cheapest model in {Haiku 4.5, Sonnet 4.6, -//! Opus 4.7} that meets the empirical quality bar for similar past -//! invocations. Reads from `kei-ledger` posterior, writes back outcomes. +//! Reads three TOML registries (providers / models / agent-profiles) and +//! exposes two selection surfaces: //! -//! Constructor Pattern: each cube under 200 LOC, each function under 30. -//! Cubes assembled here: +//! - `pick(profile_id, registry)` — registry-backed profile resolution. +//! - `select(input, conn)` — empirical posterior + cost argmin. //! -//! - `pricing` — verified per-MTok constants (RULE 0.4, 2026-04-30) -//! - `dna_class` — task-class DNA extraction (strip nonce/body suffixes) -//! - `complexity` — τ-estimator (regex+length+role heuristics) -//! - `posterior` — Beta posterior from ledger rows per (task-class, model) -//! - `kernel` — substrate similarity for unseen task classes -//! - `select` — decision rule: argmin cost s.t. P[q ≥ q*] ≥ 1−δ -//! - `escalate` — retry-ladder bookkeeping -//! -//! Distinct from `kei-router` (which handles NL→tool dispatch and -//! generic LLM provider abstraction). This crate's only job is selecting -//! WHICH Claude tier to spawn an Agent on. +//! Constructor Pattern: one file = one responsibility. +//! Cubes: +//! - `registry_types` — Provider / Model / Profile TOML structs +//! - `registry` — Registry loader + lookup methods +//! - `pricing` — cost_micro_cents + legacy Model enum +//! - `dna_class` — task-class DNA extraction +//! - `complexity` — τ-estimator (heuristic) +//! - `posterior` — Beta posterior from ledger +//! - `kernel` — DNA similarity kernel +//! - `select` — pick() types + thin delegation +//! - `select_posterior` — empirical posterior argmin logic +//! - `select_kernel` — SQL kernel-smoothing fallback +//! - `escalate` — next_model() + legacy escalation ladder +//! - `calibrate` — offline kernel-weight calibration pub mod calibrate; pub mod complexity; @@ -27,14 +28,26 @@ pub mod escalate; pub mod kernel; pub mod posterior; pub mod pricing; +pub mod registry; +pub mod registry_types; pub mod select; +pub(crate) mod select_kernel; +pub(crate) mod select_posterior; +// Registry API +pub use registry::Registry; +pub use registry_types::{Model as RegistryModel, Profile, Provider}; + +// Pricing API +pub use pricing::{cost_micro_cents, Model, OPUS_47_TOKENIZER_OVERHEAD}; + +// Selection API +pub use select::{pick, select, Decision, DecisionInput}; + +// Escalation API +pub use escalate::{next_model, next_after_failure, EscalationDecision, MAX_ESCALATION_DEPTH}; + +// Utility re-exports pub use complexity::{ComplexityEstimate, Tier}; -pub use escalate::{next_after_failure, EscalationDecision, MAX_ESCALATION_DEPTH}; pub use kernel::{similarity, KernelWeights}; pub use posterior::Posterior; -pub use pricing::{ - cost_micro_cents, Model, ModelPricing, HAIKU_45, OPUS_47, OPUS_47_TOKENIZER_OVERHEAD, - SONNET_46, -}; -pub use select::{select, Decision, DecisionInput}; diff --git a/_primitives/_rust/kei-model-router/src/main.rs b/_primitives/_rust/kei-model-router/src/main.rs index 4070ed6..8956cf0 100644 --- a/_primitives/_rust/kei-model-router/src/main.rs +++ b/_primitives/_rust/kei-model-router/src/main.rs @@ -1,16 +1,15 @@ //! kei-model-router CLI. //! //! Subcommands: -//! pricing — print verified pricing table (default) +//! pricing — print pricing table from models.toml //! select [--prompt P] -//! — query router decision for given agent -//! spawn. Reads ledger at $KEI_LEDGER_DB. -//! calibrate — re-fit kernel weights against ledger -//! outcomes. Print baseline vs best MSE. +//! — query router decision for given agent spawn +//! calibrate — re-fit kernel weights against ledger outcomes //! --help use kei_model_router::{ - calibrate, select, DecisionInput, KernelWeights, Model, OPUS_47_TOKENIZER_OVERHEAD, + calibrate, pick, select, DecisionInput, KernelWeights, Model, Registry, + OPUS_47_TOKENIZER_OVERHEAD, }; use rusqlite::Connection; @@ -30,74 +29,54 @@ fn main() { } fn print_help() { - println!("kei-model-router — model selection for Claude Code Agent spawns"); - println!(); - println!("Usage:"); - println!(" kei-model-router [pricing] print verified pricing table"); - println!(" kei-model-router select [--prompt P]"); - println!(" route a synthetic spawn"); - println!(" kei-model-router calibrate re-fit kernel weights"); - println!(" kei-model-router --help"); - println!(); - println!("Env:"); - println!(" KEI_LEDGER_DB override ledger path"); - println!(" (default: ~/.claude/agents/ledger.sqlite)"); + print!(concat!( + "kei-model-router — model selection for Claude Code Agent spawns\n\n", + "Usage:\n", + " kei-model-router [pricing] print pricing table from models.toml\n", + " kei-model-router select [--prompt P]\n", + " kei-model-router calibrate re-fit kernel weights\n", + " kei-model-router --help\n\n", + "Env:\n", + " KEI_LEDGER_DB override ledger path\n", + " KEI_REGISTRIES_DIR override registries dir\n", + )); } fn print_pricing() { - println!("kei-model-router — verified Claude API pricing (microcents per 1M tokens)"); - println!("Source: https://platform.claude.com/docs/en/docs/about-claude/pricing"); - println!("Verified: 2026-04-30 (RULE 0.4)"); - println!(); - println!( - "{:<10} {:>12} {:>12} {:>12} {:>12}", - "model", "input/M", "output/M", "cache_w_5m", "cache_r" - ); - for m in Model::all() { - let p = m.pricing(); + let reg = match Registry::load() { + Ok(r) => r, + Err(e) => { eprintln!("registry load error: {e}"); std::process::exit(1); } + }; + println!("kei-model-router — pricing from models.toml\n"); + println!("{:<30} {:>12} {:>12} {:>12}", "model", "input/M", "output/M", "cache_r"); + for m in ®.models { println!( - "{:<10} {:>12} {:>12} {:>12} {:>12}", - m.slug(), - fmt_microcents(p.input_micro_cents_per_mtok), - fmt_microcents(p.output_micro_cents_per_mtok), - fmt_microcents(p.cache_write_5m_micro_cents_per_mtok), - fmt_microcents(p.cache_read_micro_cents_per_mtok), + "{:<30} {:>12} {:>12} {:>12}", + m.id, + fmt_micro(m.cost_input_per_mtok_micro), + fmt_micro(m.cost_output_per_mtok_micro), + fmt_micro(m.cache_read_per_mtok_micro), ); } - println!(); - println!( - "Note: Opus 4.7 tokenizer may use up to {:.0}% more tokens", - (OPUS_47_TOKENIZER_OVERHEAD - 1.0) * 100.0 - ); - println!("on identical text vs Sonnet/Haiku; multiply Opus quote accordingly."); + println!("\nNote: Opus 4.7 tokenizer may use up to {:.0}% more tokens vs Sonnet/Haiku.", + (OPUS_47_TOKENIZER_OVERHEAD - 1.0) * 100.0); } fn cmd_select(args: &[String]) { - let agent = match args.first() { - Some(a) => a, - None => { - eprintln!("usage: kei-model-router select [--prompt PROMPT]"); - std::process::exit(2); + let agent = args.first().unwrap_or_else(|| { + eprintln!("usage: kei-model-router select [--prompt PROMPT]"); + std::process::exit(2); + }); + let prompt = parse_prompt_flag(args); + + if let Ok(reg) = Registry::load() { + if let Some((prov, model)) = pick(agent, ®) { + println!("agent: {agent}\nprovider: {prov}\nmodel: {model}\nreason: profile_default_model_ref"); + return; } - }; - let mut prompt = String::new(); - let mut i = 1; - while i < args.len() { - if args[i] == "--prompt" { - if let Some(p) = args.get(i + 1) { - prompt = p.clone(); - i += 2; - continue; - } - } - i += 1; } - // Synthesize a DNA from agent name. Real spawns get DNA from - // agent-fork-logger.sh via kei-shared::compose_dna; this CLI uses - // a stable synthetic so users can probe without a real spawn. let synthetic_dna = format!("{agent}::?::00000000::00000000-00000000"); - let conn = match open_ledger() { Some(c) => c, None => { @@ -110,73 +89,57 @@ fn cmd_select(args: &[String]) { let mut input = DecisionInput::new(synthetic_dna.clone(), prompt); input.kernel_weights = KernelWeights::default(); input.pinned = read_pinned_for_agent(agent); - let decision = match select(&input, &conn) { + let d = match select(&input, &conn) { Ok(d) => d, - Err(e) => { - eprintln!("ledger query failed: {e}"); - std::process::exit(1); - } + Err(e) => { eprintln!("ledger query failed: {e}"); std::process::exit(1); } }; println!("agent: {agent}"); - println!("dna: {synthetic_dna}"); - println!("model: {}", decision.model.slug()); - println!( - "expected_cost ${:.4} (microcents={})", - decision.expected_cost_micro_cents as f64 / 100_000_000.0, - decision.expected_cost_micro_cents - ); - println!( - "q_lower_bound {:.3} (posterior n={})", - decision.quality_lower_bound, decision.posterior_n - ); - println!( - "complexity τ={:.2} ({:?} signals)", - decision.complexity.tau, decision.complexity.features - ); - println!("reason: {}", decision.reason); + println!("model: {}", d.model.slug()); + println!("expected_cost ${:.4} (microcents={})", + d.expected_cost_micro_cents as f64 / 100_000_000.0, d.expected_cost_micro_cents); + println!("q_lower_bound {:.3} (posterior n={})", d.quality_lower_bound, d.posterior_n); + println!("reason: {}", d.reason); +} + +fn parse_prompt_flag(args: &[String]) -> String { + let mut i = 1; + while i < args.len() { + if args[i] == "--prompt" { + if let Some(p) = args.get(i + 1) { return p.clone(); } + } + i += 1; + } + String::new() } fn print_decision_no_ledger(dna: &str, prompt: &str) { let inp = DecisionInput::new(dna.to_string(), prompt.to_string()); - let est = kei_model_router::complexity::estimate(prompt, kei_model_router::dna_class::role(dna)); - println!("model: {}", inp.fallback.slug()); - println!("τ: {:.2}", est.tau); - println!("reason: no_ledger_fallback"); + let est = kei_model_router::complexity::estimate( + prompt, kei_model_router::dna_class::role(dna)); + println!("model: {}\nτ: {:.2}\nreason: no_ledger_fallback", + inp.fallback.slug(), est.tau); } fn cmd_calibrate() { let conn = match open_ledger() { Some(c) => c, - None => { - eprintln!("ledger not found; aborting calibration"); - std::process::exit(1); - } + None => { eprintln!("ledger not found; aborting calibration"); std::process::exit(1); } }; - let result = match calibrate::calibrate(&conn) { + let r = match calibrate::calibrate(&conn) { Ok(r) => r, - Err(e) => { - eprintln!("calibration query failed: {e}"); - std::process::exit(1); - } + Err(e) => { eprintln!("calibration query failed: {e}"); std::process::exit(1); } }; - println!("rows evaluated: {}", result.rows_evaluated); - if result.rows_evaluated < 5 { + println!("rows evaluated: {}", r.rows_evaluated); + if r.rows_evaluated < 5 { println!("(too few rows for calibration; using default weights)"); return; } - println!("baseline MSE: {:.4}", result.baseline_mse); - println!("best MSE: {:.4}", result.best_mse); - println!( - "improvement: {:.4}", - result.baseline_mse - result.best_mse - ); - println!(); - println!("calibrated weights:"); - println!(" alpha_role: {:.2}", result.best_weights.alpha_role); - println!(" alpha_caps: {:.2}", result.best_weights.alpha_caps); - println!(" alpha_scope: {:.2}", result.best_weights.alpha_scope); - println!(" alpha_body: {:.2}", result.best_weights.alpha_body); + println!("baseline MSE: {:.4}\nbest MSE: {:.4}\nimprovement: {:.4}", + r.baseline_mse, r.best_mse, r.baseline_mse - r.best_mse); + println!("calibrated weights:\n alpha_role: {:.2}\n alpha_caps: {:.2}\n alpha_scope: {:.2}\n alpha_body: {:.2}", + r.best_weights.alpha_role, r.best_weights.alpha_caps, + r.best_weights.alpha_scope, r.best_weights.alpha_body); } fn open_ledger() -> Option { @@ -187,20 +150,14 @@ fn open_ledger() -> Option { Connection::open(&path).ok() } -/// Read `~/.claude/settings.json::router.pinned[agent]` if present. -/// Returns Some(Model) for agents the user has pinned to a specific tier. -/// Examples: "Explore" → Haiku, "ml-implementer" → Opus. fn read_pinned_for_agent(agent: &str) -> Option { let home = std::env::var("HOME").ok()?; - let path = format!("{home}/.claude/settings.json"); - let raw = std::fs::read_to_string(&path).ok()?; + let raw = std::fs::read_to_string(format!("{home}/.claude/settings.json")).ok()?; let json: serde_json::Value = serde_json::from_str(&raw).ok()?; - let pinned = json.get("router")?.get("pinned")?; - let model_slug = pinned.get(agent)?.as_str()?; + let model_slug = json.get("router")?.get("pinned")?.get(agent)?.as_str()?; Model::from_slug(model_slug) } -fn fmt_microcents(uc: u64) -> String { - let dollars = uc as f64 / 100_000_000.0; - format!("${:.2}", dollars) +fn fmt_micro(uc: u64) -> String { + format!("${:.2}", uc as f64 / 100_000_000.0) } diff --git a/_primitives/_rust/kei-model-router/src/posterior.rs b/_primitives/_rust/kei-model-router/src/posterior.rs index 9c1e092..4f372a7 100644 --- a/_primitives/_rust/kei-model-router/src/posterior.rs +++ b/_primitives/_rust/kei-model-router/src/posterior.rs @@ -1,17 +1,13 @@ //! Beta posterior over per-(task-class, model) success rate. //! //! For each (task_class_dna, model) pair in the ledger we count: -//! n+ = rows with outcome='functional' AND escalation_depth=0 (clean wins) -//! n- = rows with anything else (partial, scaffolding, fail, retry) +//! n+ = rows with outcome='functional' AND escalation_depth=0 +//! n- = rows with anything else //! -//! Posterior on success probability q ∼ Beta(α₀ + n+, β₀ + n-) with -//! uniform prior α₀ = β₀ = 1. Confidence-bounded lower estimate -//! `q_lower(δ)` returned via the inverse-Beta CDF approximation -//! (Wilson-style normal approx — adequate for our regime where n is -//! typically small but δ ≈ 0.10). +//! Model identity is keyed by `Model::slug()` — the canonical model id +//! string (e.g. `claude-sonnet-4-6`) stored in `agents.model`. //! -//! Constructor Pattern: SQL is one query, math is pure-fn, -//! `Posterior::from_ledger` is the only DB-touching surface. +//! Constructor Pattern: SQL is one query, math is pure-fn. use crate::pricing::Model; use rusqlite::{params, Connection, OptionalExtension, Result as SqlResult}; @@ -24,11 +20,7 @@ pub struct Posterior { } impl Posterior { - pub const PRIOR: Posterior = Posterior { - alpha: 1.0, - beta: 1.0, - n: 0, - }; + pub const PRIOR: Posterior = Posterior { alpha: 1.0, beta: 1.0, n: 0 }; /// Posterior mean q̄ = α / (α + β). pub fn mean(&self) -> f64 { @@ -41,40 +33,23 @@ impl Posterior { (self.alpha * self.beta) / (s * s * (s + 1.0)) } - /// Wilson-style normal-approx lower confidence bound: - /// q_lower = mean − z(1−δ) · sqrt(var). Floor at 0, cap at 1. - /// Adequate when n ≥ 5; for smaller n the prior dominates and bound - /// is conservative (i.e., very low) which biases toward Opus — desired - /// behavior under uncertainty per RULE -1. + /// Wilson-style normal-approx lower confidence bound. pub fn quality_lower_bound(&self, delta: f64) -> f64 { let z = z_one_sided(delta); let lb = self.mean() - z * self.variance().sqrt(); lb.clamp(0.0, 1.0) } - /// Bayesian update with new observation (success ⇒ α+1, failure ⇒ β+1). + /// Bayesian update with new observation. pub fn observe(self, success: bool) -> Self { if success { - Self { - alpha: self.alpha + 1.0, - beta: self.beta, - n: self.n + 1, - } + Self { alpha: self.alpha + 1.0, beta: self.beta, n: self.n + 1 } } else { - Self { - alpha: self.alpha, - beta: self.beta + 1.0, - n: self.n + 1, - } + Self { alpha: self.alpha, beta: self.beta + 1.0, n: self.n + 1 } } } - /// Build posterior from ledger rows for a given (task_class_dna, model). - /// Counts rows where: - /// success := outcome='functional' AND COALESCE(escalation_depth, 0) = 0 - /// failure := everything else with non-NULL outcome - /// Rows with NULL outcome (legacy / in-progress) are skipped — they - /// don't update the posterior but don't bias it either. + /// Build posterior from ledger rows for (task_class_dna, model). pub fn from_ledger( conn: &Connection, task_class: &str, @@ -93,8 +68,10 @@ impl Posterior { FROM agents WHERE task_class_dna = ?1 AND model = ?2", params![task_class, model.slug()], - |r| Ok((r.get::<_, Option>(0)?.unwrap_or(0), - r.get::<_, Option>(1)?.unwrap_or(0))), + |r| Ok(( + r.get::<_, Option>(0)?.unwrap_or(0), + r.get::<_, Option>(1)?.unwrap_or(0), + )), ) .optional()?; let (n_plus, n_minus) = row.unwrap_or((0, 0)); @@ -106,10 +83,6 @@ impl Posterior { } } -/// One-sided z-score for confidence (1−δ). Approximates inverse normal -/// CDF for δ ∈ {0.01, 0.05, 0.10, 0.20}. For other δ uses a coarse -/// Newton-Raphson around the standard table values. Sufficient for the -/// router's needs — we never need finer than 1% steps. fn z_one_sided(delta: f64) -> f64 { match delta { d if d <= 0.01 => 2.326, @@ -129,11 +102,8 @@ mod tests { let c = Connection::open_in_memory().unwrap(); c.execute_batch( "CREATE TABLE agents ( - id TEXT, - task_class_dna TEXT, - model TEXT, - outcome TEXT, - escalation_depth INTEGER DEFAULT 0 + id TEXT, task_class_dna TEXT, model TEXT, + outcome TEXT, escalation_depth INTEGER DEFAULT 0 );", ) .unwrap(); @@ -169,29 +139,27 @@ mod tests { #[test] fn ledger_aggregates_by_model_slug() { let c = fresh_db(); + // Use canonical model ids (matching Model::slug()) + let haiku = Model::Haiku45.slug(); + let opus = Model::Opus47.slug(); c.execute( - "INSERT INTO agents VALUES ('1','tc1','haiku','functional',0)", - [], - ) - .unwrap(); + "INSERT INTO agents VALUES ('1','tc1',?1,'functional',0)", + rusqlite::params![haiku], + ).unwrap(); c.execute( - "INSERT INTO agents VALUES ('2','tc1','haiku','functional',0)", - [], - ) - .unwrap(); + "INSERT INTO agents VALUES ('2','tc1',?1,'functional',0)", + rusqlite::params![haiku], + ).unwrap(); c.execute( - "INSERT INTO agents VALUES ('3','tc1','haiku','partial',0)", - [], - ) - .unwrap(); + "INSERT INTO agents VALUES ('3','tc1',?1,'partial',0)", + rusqlite::params![haiku], + ).unwrap(); c.execute( - "INSERT INTO agents VALUES ('4','tc1','opus','functional',0)", - [], - ) - .unwrap(); + "INSERT INTO agents VALUES ('4','tc1',?1,'functional',0)", + rusqlite::params![opus], + ).unwrap(); let h = Posterior::from_ledger(&c, "tc1", Model::Haiku45).unwrap(); assert_eq!(h.n, 3); - // 2 successes + 1 failure → α=3, β=2, mean=0.6 assert!((h.mean() - 0.6).abs() < 1e-9); let o = Posterior::from_ledger(&c, "tc1", Model::Opus47).unwrap(); assert_eq!(o.n, 1); @@ -200,13 +168,12 @@ mod tests { #[test] fn escalated_success_counts_as_failure_for_first_pass() { let c = fresh_db(); + let slug = Model::Haiku45.slug(); c.execute( - "INSERT INTO agents VALUES ('1','tc','haiku','functional',1)", - [], - ) - .unwrap(); + "INSERT INTO agents VALUES ('1','tc',?1,'functional',1)", + rusqlite::params![slug], + ).unwrap(); let p = Posterior::from_ledger(&c, "tc", Model::Haiku45).unwrap(); - // depth>0 ⇒ counted in n_minus assert_eq!(p.alpha, 1.0); assert_eq!(p.beta, 2.0); } @@ -218,7 +185,6 @@ mod tests { p = p.observe(true); } let lb = p.quality_lower_bound(0.10); - // mean ≈ 101/102 ≈ 0.99; lb should still be > 0.95 assert!(lb > 0.95, "lb={}", lb); } @@ -226,7 +192,6 @@ mod tests { fn lower_bound_with_no_data_is_conservative() { let p = Posterior::PRIOR; let lb = p.quality_lower_bound(0.10); - // mean=0.5, var=1/12 ≈ 0.083, sqrt ≈ 0.289, z=1.282 → lb ≈ 0.13 assert!(lb < 0.30); } } diff --git a/_primitives/_rust/kei-model-router/src/pricing.rs b/_primitives/_rust/kei-model-router/src/pricing.rs index c03b767..538ec01 100644 --- a/_primitives/_rust/kei-model-router/src/pricing.rs +++ b/_primitives/_rust/kei-model-router/src/pricing.rs @@ -1,61 +1,49 @@ -//! Verified Claude API pricing constants. +//! Pricing helpers — registry-backed cost computation. //! -//! Source: -//! Verified: 2026-04-30 (RULE 0.4 — primary source fetched in same session). +//! Source of truth: `models.toml` via `registry::Registry`. +//! All prices are in microcents per 1M tokens (u64) to avoid float drift. +//! 1 microcent = 1e-6 USD = 1e-4 cents. //! -//! All prices in microcents per 1M tokens (`u64` to avoid float drift in -//! cost arithmetic). 1 microcent = 1e-6 USD = 1e-4 cents. Aligns with -//! `kei-ledger.cost_micro_cents` column. +//! `cost_micro_cents(model_id, tokens_in, tokens_out, registry)` is the +//! primary entry point; returns None if model_id is unknown. //! -//! Constructor Pattern: pricing is one cube. The decision rule (`select.rs`) -//! reads constants from here and never duplicates them. +//! Legacy `Model` enum is kept for `posterior.rs` / `calibrate.rs` which +//! still use model slugs for SQL ledger queries. New code should use model +//! id strings from the registry directly. +//! +//! Constructor Pattern: pricing is one cube. Decision rule (`select.rs`) +//! reads from here and never duplicates cost arithmetic. -/// Per-model token pricing (microcents per 1M tokens). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ModelPricing { - pub input_micro_cents_per_mtok: u64, - pub output_micro_cents_per_mtok: u64, - pub cache_write_5m_micro_cents_per_mtok: u64, - pub cache_read_micro_cents_per_mtok: u64, +use crate::registry::Registry; + +/// Compute cost in microcents for one (input, output) token pair. +/// +/// Returns `None` if `model_id` is not present in the registry. +/// Does NOT account for cache hits / batch discounts — those are applied +/// by callers as orthogonal multipliers. +pub fn cost_micro_cents( + model_id: &str, + tokens_in: u64, + tokens_out: u64, + registry: &Registry, +) -> Option { + let m = registry.model_by_id(model_id)?; + let input = tokens_in.saturating_mul(m.cost_input_per_mtok_micro) / 1_000_000; + let output = tokens_out.saturating_mul(m.cost_output_per_mtok_micro) / 1_000_000; + Some(input.saturating_add(output)) } -/// Tokenizer density relative to baseline (Sonnet/Haiku tokenizer). -/// -/// Opus 4.7 ships a new tokenizer that may produce up to 35% more tokens -/// on the same source text [VERIFIED: pricing page 2026-04-30 note]. -/// Multiply expected token count by this when comparing Opus 4.7 to other -/// models on identical text input. +/// Tokenizer density of Opus 4.7 relative to Sonnet/Haiku baseline. +/// Multiply expected token count by this when comparing Opus 4.7 to +/// other models on identical text input. pub const OPUS_47_TOKENIZER_OVERHEAD: f64 = 1.35; -/// Claude Haiku 4.5 — cheapest, simple lookup / formatting / single-edit. -pub const HAIKU_45: ModelPricing = ModelPricing { - input_micro_cents_per_mtok: 100_000_000, // $1.00 - output_micro_cents_per_mtok: 500_000_000, // $5.00 - cache_write_5m_micro_cents_per_mtok: 125_000_000, // $1.25 - cache_read_micro_cents_per_mtok: 10_000_000, // $0.10 -}; +// ────────────────────────────────────────────────────────────────────────────── +// Legacy Model enum — kept for posterior.rs + calibrate.rs SQL lookup by slug. +// Do NOT use in new code; reference registry model ids directly. +// ────────────────────────────────────────────────────────────────────────────── -/// Claude Sonnet 4.6 — multi-step reasoning, code edits, summarization. -pub const SONNET_46: ModelPricing = ModelPricing { - input_micro_cents_per_mtok: 300_000_000, // $3.00 - output_micro_cents_per_mtok: 1_500_000_000, // $15.00 - cache_write_5m_micro_cents_per_mtok: 375_000_000, // $3.75 - cache_read_micro_cents_per_mtok: 30_000_000, // $0.30 -}; - -/// Claude Opus 4.7 — architecture, novel reasoning, math derivation. -/// -/// 4.5/4.6/4.7 are at the SAME price point — half the rate of Opus 4.1 -/// (which was $15/$75). [VERIFIED: pricing table 2026-04-30]. -pub const OPUS_47: ModelPricing = ModelPricing { - input_micro_cents_per_mtok: 500_000_000, // $5.00 - output_micro_cents_per_mtok: 2_500_000_000, // $25.00 - cache_write_5m_micro_cents_per_mtok: 625_000_000, // $6.25 - cache_read_micro_cents_per_mtok: 50_000_000, // $0.50 -}; - -/// Discrete model identifier. Order matches escalation ladder -/// (cheaper first → richer last). +/// Discrete Claude model identifier (legacy). Order = escalation ladder. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum Model { Haiku45, @@ -64,28 +52,11 @@ pub enum Model { } impl Model { - pub fn pricing(&self) -> ModelPricing { - match self { - Self::Haiku45 => HAIKU_45, - Self::Sonnet46 => SONNET_46, - Self::Opus47 => OPUS_47, - } - } - pub fn slug(&self) -> &'static str { match self { - Self::Haiku45 => "haiku", - Self::Sonnet46 => "sonnet", - Self::Opus47 => "opus", - } - } - - /// Next-tier (escalation). Returns None if already at top. - pub fn next_tier(&self) -> Option { - match self { - Self::Haiku45 => Some(Self::Sonnet46), - Self::Sonnet46 => Some(Self::Opus47), - Self::Opus47 => None, + Self::Haiku45 => "claude-haiku-4-5", + Self::Sonnet46 => "claude-sonnet-4-6", + Self::Opus47 => "claude-opus-4-7", } } @@ -103,44 +74,48 @@ impl Model { } } -/// Cost in microcents for a single (input, output) token pair on `model`. -/// Does NOT account for cache hits / batch discount / data residency -/// modifiers — those are orthogonal multipliers applied by callers. -pub fn cost_micro_cents(model: Model, tokens_in: u64, tokens_out: u64) -> u64 { - let p = model.pricing(); - let input = tokens_in.saturating_mul(p.input_micro_cents_per_mtok) / 1_000_000; - let output = tokens_out.saturating_mul(p.output_micro_cents_per_mtok) / 1_000_000; - input.saturating_add(output) -} - #[cfg(test)] mod tests { use super::*; + use std::path::PathBuf; - #[test] - fn opus_47_input_is_5_dollars_per_mtok() { - // 1M tokens at $5 = 500M microcents - assert_eq!(cost_micro_cents(Model::Opus47, 1_000_000, 0), 500_000_000); + fn reg() -> Registry { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap() + .parent().unwrap() + .parent().unwrap() + .join("_blocks/registries"); + Registry::load_from(&dir).expect("registry load failed") } #[test] - fn haiku_output_is_5_dollars_per_mtok() { - assert_eq!(cost_micro_cents(Model::Haiku45, 0, 1_000_000), 500_000_000); + fn sonnet_mixed_cost_matches_toml() { + // 100k in + 50k out: + // input: 100_000 * 300_000_000 / 1_000_000 = 30_000_000 + // output: 50_000 * 1_500_000_000 / 1_000_000 = 75_000_000 + let r = reg(); + let c = cost_micro_cents("claude-sonnet-4-6", 100_000, 50_000, &r).unwrap(); + assert_eq!(c, 30_000_000 + 75_000_000, "got {c}"); } #[test] - fn sonnet_mixed_input_output() { - // 100k in + 50k out at Sonnet rates: 100k*$3/MTok + 50k*$15/MTok - // = $0.30 + $0.75 = $1.05 = 105M microcents - let c = cost_micro_cents(Model::Sonnet46, 100_000, 50_000); - assert_eq!(c, 30_000_000 + 75_000_000); + fn opus_input_1m_is_500m_microcents() { + let r = reg(); + let c = cost_micro_cents("claude-opus-4-7", 1_000_000, 0, &r).unwrap(); + assert_eq!(c, 500_000_000); } #[test] - fn next_tier_terminates_at_opus() { - assert_eq!(Model::Haiku45.next_tier(), Some(Model::Sonnet46)); - assert_eq!(Model::Sonnet46.next_tier(), Some(Model::Opus47)); - assert_eq!(Model::Opus47.next_tier(), None); + fn haiku_output_1m_is_500m_microcents() { + let r = reg(); + let c = cost_micro_cents("claude-haiku-4-5", 0, 1_000_000, &r).unwrap(); + assert_eq!(c, 500_000_000); + } + + #[test] + fn unknown_model_returns_none() { + let r = reg(); + assert!(cost_micro_cents("does-not-exist", 1_000, 1_000, &r).is_none()); } #[test] @@ -149,20 +124,4 @@ mod tests { assert_eq!(Model::from_slug(m.slug()), Some(m)); } } - - #[test] - fn opus_is_5x_haiku_input_3x_sonnet_at_modern_pricing() { - // 2026-04-30 pricing audit lock-in: spreads matter for routing - // economics. If Anthropic re-prices and these assertions break, - // re-verify the pricing page and update constants + this test. - assert_eq!( - OPUS_47.input_micro_cents_per_mtok, - 5 * HAIKU_45.input_micro_cents_per_mtok, - "Opus 4.7 must be 5x Haiku 4.5 input — re-verify pricing if this fails" - ); - assert_eq!( - OPUS_47.output_micro_cents_per_mtok, - 5 * HAIKU_45.output_micro_cents_per_mtok - ); - } } diff --git a/_primitives/_rust/kei-model-router/src/registry.rs b/_primitives/_rust/kei-model-router/src/registry.rs new file mode 100644 index 0000000..124f553 --- /dev/null +++ b/_primitives/_rust/kei-model-router/src/registry.rs @@ -0,0 +1,152 @@ +//! Registry loader — reads providers.toml, models.toml, agent-profiles.toml. +//! +//! Path resolution: +//! 1. `KEI_REGISTRIES_DIR` env var (if set) +//! 2. `~/Projects/KeiSeiKit-public/_blocks/registries/` (default) +//! +//! Types live in `registry_types.rs` (separate cube per Constructor Pattern). +//! This cube owns loading + lookup methods only. + +use serde::de::DeserializeOwned; +use std::path::{Path, PathBuf}; + +pub use crate::registry_types::{Model, Profile, Provider}; +use crate::registry_types::{ModelsFile, ProfilesFile, ProvidersFile}; + +#[derive(Debug, Clone)] +pub struct Registry { + pub providers: Vec, + pub models: Vec, + pub profiles: Vec, +} + +impl Registry { + /// Load all three TOML files from `dir`. + pub fn load_from(dir: &Path) -> Result> { + let providers = parse_toml::(&dir.join("providers.toml"))?.provider; + let models = parse_toml::(&dir.join("models.toml"))?.model; + let profiles = + parse_toml::(&dir.join("agent-profiles.toml"))?.profile; + Ok(Self { providers, models, profiles }) + } + + /// Load from `KEI_REGISTRIES_DIR` or the project-default path. + pub fn load() -> Result> { + Self::load_from(®istries_dir()) + } + + pub fn provider_by_id(&self, id: &str) -> Option<&Provider> { + self.providers.iter().find(|p| p.id == id) + } + + pub fn model_by_id(&self, id: &str) -> Option<&Model> { + self.models.iter().find(|m| m.id == id) + } + + pub fn profile_by_id(&self, id: &str) -> Option<&Profile> { + self.profiles.iter().find(|p| p.id == id) + } + + /// All non-deprecated models for a provider, sorted by output cost ascending. + pub fn models_for_provider(&self, provider_id: &str) -> Vec<&Model> { + let mut ms: Vec<&Model> = self + .models + .iter() + .filter(|m| m.provider_ref == provider_id && !m.is_deprecated()) + .collect(); + ms.sort_by_key(|m| m.cost_output_per_mtok_micro); + ms + } +} + +fn registries_dir() -> PathBuf { + if let Ok(v) = std::env::var("KEI_REGISTRIES_DIR") { + return PathBuf::from(v); + } + let home = std::env::var("HOME").unwrap_or_default(); + PathBuf::from(format!( + "{home}/Projects/KeiSeiKit-public/_blocks/registries" + )) +} + +fn parse_toml(path: &Path) -> Result> { + let raw = std::fs::read_to_string(path) + .map_err(|e| format!("cannot read {}: {e}", path.display()))?; + let parsed: T = toml::from_str(&raw) + .map_err(|e| format!("cannot parse {}: {e}", path.display()))?; + Ok(parsed) +} + +// ────────────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn reg() -> Registry { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap() // _rust/ + .parent().unwrap() // _primitives/ + .parent().unwrap() // KeiSeiKit-public/ + .join("_blocks/registries"); + Registry::load_from(&dir).expect("registry load failed") + } + + #[test] + fn loads_all_three_files() { + let r = reg(); + assert!(!r.providers.is_empty(), "providers empty"); + assert!(!r.models.is_empty(), "models empty"); + assert!(!r.profiles.is_empty(), "profiles empty"); + } + + #[test] + fn provider_by_id_anthropic() { + let r = reg(); + let p = r.provider_by_id("anthropic").expect("anthropic missing"); + assert_eq!(p.display_name, "Anthropic"); + } + + #[test] + fn model_by_id_sonnet() { + let r = reg(); + let m = r.model_by_id("claude-sonnet-4-6").expect("sonnet missing"); + assert_eq!(m.provider_ref, "anthropic"); + assert_eq!(m.cost_input_per_mtok_micro, 300_000_000); + assert_eq!(m.cost_output_per_mtok_micro, 1_500_000_000); + } + + #[test] + fn profile_by_id_code_implementer_rust() { + let r = reg(); + let p = r.profile_by_id("code-implementer-rust").expect("profile missing"); + let (provider, model) = p.split_model_ref().expect("split failed"); + assert_eq!(provider, "anthropic"); + assert_eq!(model, "claude-sonnet-4-6"); + } + + #[test] + fn models_for_provider_sorted_by_output_cost() { + let r = reg(); + let ms = r.models_for_provider("anthropic"); + assert!(ms.len() >= 3, "expected >= 3 anthropic models"); + for w in ms.windows(2) { + assert!( + w[0].cost_output_per_mtok_micro <= w[1].cost_output_per_mtok_micro, + "not sorted: {} > {}", + w[0].id, w[1].id + ); + } + } + + #[test] + fn deprecated_models_excluded_from_provider_list() { + let r = reg(); + let ms = r.models_for_provider("anthropic"); + for m in ms { + assert!(!m.is_deprecated(), "{} should not be deprecated", m.id); + } + } +} diff --git a/_primitives/_rust/kei-model-router/src/registry_types.rs b/_primitives/_rust/kei-model-router/src/registry_types.rs new file mode 100644 index 0000000..b227546 --- /dev/null +++ b/_primitives/_rust/kei-model-router/src/registry_types.rs @@ -0,0 +1,107 @@ +//! TOML wire types for the three registry files. +//! +//! One module per layer (providers, models, profiles). Kept separate from +//! Registry loading logic so the struct definitions are easy to navigate. +//! +//! Constructor Pattern: types-before-implementation. This cube defines +//! WHAT; `registry.rs` defines HOW to load and look them up. + +use serde::Deserialize; + +// ────────────────────────────────────────────────────────────────────────────── +// Layer 1: providers.toml +// ────────────────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Deserialize)] +pub struct Provider { + pub id: String, + pub display_name: String, + pub endpoint: String, + pub auth_scheme: String, + pub auth_env: String, + pub retry_max: u32, + pub retry_backoff_ms: u32, + pub rate_limit_rpm: u32, + pub billing_currency: String, + pub notes: String, + #[serde(default)] + pub api_version_header: String, + #[serde(default)] + pub api_version_value: String, +} + +// ────────────────────────────────────────────────────────────────────────────── +// Layer 2: models.toml +// ────────────────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Deserialize)] +pub struct Model { + pub provider_ref: String, + pub id: String, + pub slug: String, + pub display_name: String, + pub context_window: u64, + /// Microcents per 1M input tokens. Aligns with kei-ledger.cost_micro_cents. + pub cost_input_per_mtok_micro: u64, + /// Microcents per 1M output tokens. + pub cost_output_per_mtok_micro: u64, + pub cache_write_5m_per_mtok_micro: u64, + pub cache_read_per_mtok_micro: u64, + #[serde(default)] + pub verified_at: String, + /// Empty string = live. Non-empty = deprecated since that date. + #[serde(default)] + pub deprecated_at: String, + #[serde(default)] + pub notes: String, +} + +impl Model { + /// True when this model should be excluded from new invocations. + pub fn is_deprecated(&self) -> bool { + !self.deprecated_at.is_empty() + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Layer 3: agent-profiles.toml +// ────────────────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Deserialize)] +pub struct Profile { + pub id: String, + pub role: String, + pub caps: String, + /// Format: `/`, e.g. `anthropic/claude-sonnet-4-6`. + pub default_model_ref: String, + pub description: String, + #[serde(default)] + pub manifest_path: String, +} + +impl Profile { + /// Split `default_model_ref` into `(provider_id, model_id)`. + /// Returns `None` if the format is not `/`. + pub fn split_model_ref(&self) -> Option<(&str, &str)> { + self.default_model_ref.split_once('/') + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// TOML envelope types (package-private; only used by registry.rs loader) +// ────────────────────────────────────────────────────────────────────────────── + +#[derive(Deserialize)] +pub(crate) struct ProvidersFile { + pub provider: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct ModelsFile { + pub model: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct ProfilesFile { + pub profile: Vec, +} diff --git a/_primitives/_rust/kei-model-router/src/select.rs b/_primitives/_rust/kei-model-router/src/select.rs index f937e59..52a6c7c 100644 --- a/_primitives/_rust/kei-model-router/src/select.rs +++ b/_primitives/_rust/kei-model-router/src/select.rs @@ -1,31 +1,43 @@ -//! Decision rule — the heart of the router. +//! Decision rule — public API for the router. //! -//! m*(d̂) = argmin_{m ∈ M} { c(d̂, m) | P[q(d̂, m) ≥ q*] ≥ 1 − δ } +//! Two surfaces: +//! - `pick(profile_id, registry)` — registry-backed profile resolution. +//! Returns `(provider_id, model_id)` from the profile's `default_model_ref`. +//! - `select(input, conn)` — empirical posterior + cost argmin. +//! Implementation lives in `select_posterior.rs`. //! -//! Implementation: -//! 1. Compute `task_class_dna` from full DNA. -//! 2. For each model m ∈ {Haiku, Sonnet, Opus}: -//! a. Pull posterior from ledger for (task_class, m). -//! b. If n=0 → optionally smooth via kernel from similar task_classes. -//! c. Compute q_lower(δ). -//! 3. Filter to models where q_lower ≥ q*. -//! 4. Among feasible: pick cheapest (smallest expected cost). -//! 5. If feasible set empty → fallback. -//! -//! Per RULE -1: empty feasible set → return fallback (top tier), NOT an -//! error. Router never refuses; it surfaces uncertainty by selecting -//! safer model. -//! -//! Constructor Pattern: this is the orchestrating cube. SQL is delegated -//! to `posterior`, math to `pricing`, similarity to `kernel`. +//! Constructor Pattern: types + thin delegation cube. -use crate::complexity::{self, ComplexityEstimate}; -use crate::dna_class; -use crate::kernel::{self, KernelWeights}; -use crate::pricing::{cost_micro_cents, Model}; -use crate::posterior::Posterior; +use crate::complexity::ComplexityEstimate; +use crate::kernel::KernelWeights; +use crate::pricing::Model; +use crate::registry::Registry; +use crate::select_posterior; use rusqlite::{Connection, Result as SqlResult}; +// ────────────────────────────────────────────────────────────────────────────── +// Registry-backed pick +// ────────────────────────────────────────────────────────────────────────────── + +/// Resolve `(provider_id, model_id)` for a given agent profile. +/// +/// Uses `profile.default_model_ref` (format `/`). +/// Returns `None` if the profile is unknown or the model is deprecated. +pub fn pick(profile_id: &str, registry: &Registry) -> Option<(String, String)> { + let profile = registry.profile_by_id(profile_id)?; + let (provider_id, model_id) = profile.split_model_ref()?; + if let Some(m) = registry.model_by_id(model_id) { + if m.is_deprecated() { + return None; + } + } + Some((provider_id.to_string(), model_id.to_string())) +} + +// ────────────────────────────────────────────────────────────────────────────── +// Types +// ────────────────────────────────────────────────────────────────────────────── + #[derive(Debug, Clone)] pub struct DecisionInput { pub full_dna: String, @@ -33,16 +45,14 @@ pub struct DecisionInput { pub q_threshold: f64, pub delta: f64, pub fallback: Model, - /// Pinned override: if Some, skip routing and use this. For per-agent pins. + /// Pinned override: if Some, skip routing and use this. pub pinned: Option, pub kernel_weights: KernelWeights, - /// Estimated input/output token counts; if None, use defaults. pub tokens_in: Option, pub tokens_out: Option, } impl DecisionInput { - /// Sensible defaults for a typical Agent spawn (~ 4k in, 1.5k out). pub const DEFAULT_TOKENS_IN: u64 = 4_000; pub const DEFAULT_TOKENS_OUT: u64 = 1_500; @@ -71,218 +81,51 @@ pub struct Decision { pub reason: &'static str, } +// ────────────────────────────────────────────────────────────────────────────── +// select() — delegates to select_posterior +// ────────────────────────────────────────────────────────────────────────────── + pub fn select(input: &DecisionInput, conn: &Connection) -> SqlResult { - let role = dna_class::role(&input.full_dna); - let complexity = complexity::estimate(&input.prompt, role); - - if let Some(m) = input.pinned { - return Ok(Decision { - model: m, - expected_cost_micro_cents: estimated_cost(input, m), - quality_lower_bound: 1.0, - posterior_n: 0, - complexity, - reason: "pinned", - }); - } - - let task_class = match dna_class::task_class_dna(&input.full_dna) { - Some(t) => t.to_string(), - None => { - return Ok(fallback_decision(input, complexity, "empty_dna")); - } - }; - - let mut feasible: Vec<(Model, Posterior, f64, u64)> = Vec::new(); - for m in Model::all() { - let mut post = Posterior::from_ledger(conn, &task_class, m)?; - if post.n == 0 { - post = smooth_via_kernel(conn, &task_class, m, input.kernel_weights)?; - } - let lb = post.quality_lower_bound(input.delta); - if lb >= input.q_threshold { - let cost = estimated_cost(input, m); - feasible.push((m, post, lb, cost)); - } - } - - if feasible.is_empty() { - return Ok(fallback_decision(input, complexity, "no_feasible")); - } - - // Cheapest feasible. - feasible.sort_by_key(|(_, _, _, c)| *c); - let (model, post, lb, cost) = feasible[0]; - Ok(Decision { - model, - expected_cost_micro_cents: cost, - quality_lower_bound: lb, - posterior_n: post.n, - complexity, - reason: "argmin_cost_feasible", - }) + select_posterior::select(input, conn) } -fn estimated_cost(input: &DecisionInput, m: Model) -> u64 { - let t_in = input.tokens_in.unwrap_or(DecisionInput::DEFAULT_TOKENS_IN); - let t_out = input.tokens_out.unwrap_or(DecisionInput::DEFAULT_TOKENS_OUT); - cost_micro_cents(m, t_in, t_out) -} - -fn fallback_decision( - input: &DecisionInput, - complexity: ComplexityEstimate, - reason: &'static str, -) -> Decision { - Decision { - model: input.fallback, - expected_cost_micro_cents: estimated_cost(input, input.fallback), - quality_lower_bound: 0.0, - posterior_n: 0, - complexity, - reason, - } -} - -/// Pull all (task_class_dna, model) posteriors weighted by kernel(task_class, *). -/// O(rows) — for large ledgers add an index-only scan; for our scale (≤10k rows) -/// this is fine. -fn smooth_via_kernel( - conn: &Connection, - target_task_class: &str, - model: Model, - weights: KernelWeights, -) -> SqlResult { - let mut stmt = conn.prepare( - "SELECT task_class_dna, - SUM(CASE WHEN outcome = 'functional' - AND COALESCE(escalation_depth, 0) = 0 - THEN 1 ELSE 0 END) AS np, - SUM(CASE WHEN outcome IS NOT NULL - AND NOT (outcome = 'functional' - AND COALESCE(escalation_depth, 0) = 0) - THEN 1 ELSE 0 END) AS nm - FROM agents - WHERE task_class_dna IS NOT NULL - AND task_class_dna != ?1 - AND model = ?2 - GROUP BY task_class_dna", - )?; - - let rows = stmt.query_map( - rusqlite::params![target_task_class, model.slug()], - |r| { - Ok(( - r.get::<_, String>(0)?, - r.get::<_, Option>(1)?.unwrap_or(0), - r.get::<_, Option>(2)?.unwrap_or(0), - )) - }, - )?; - - let mut weighted_alpha = 1.0_f64; - let mut weighted_beta = 1.0_f64; - let mut weighted_n = 0_u32; - - for row in rows { - let (other_tc, np, nm) = row?; - let sim = kernel::similarity(target_task_class, &other_tc, weights); - if sim <= 0.0 { - continue; - } - weighted_alpha += sim * np as f64; - weighted_beta += sim * nm as f64; - weighted_n = weighted_n.saturating_add((np + nm) as u32); - } - - Ok(Posterior { - alpha: weighted_alpha, - beta: weighted_beta, - n: weighted_n, - }) -} +// ────────────────────────────────────────────────────────────────────────────── +// Tests — pick() only; select() tests live in select_posterior.rs +// ────────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; - use rusqlite::Connection; + use std::path::PathBuf; - fn fresh_db_with_schema() -> Connection { - let c = Connection::open_in_memory().unwrap(); - c.execute_batch( - "CREATE TABLE agents ( - id TEXT, - task_class_dna TEXT, - model TEXT, - outcome TEXT, - escalation_depth INTEGER DEFAULT 0 - );", - ) - .unwrap(); - c + fn reg() -> Registry { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap() + .parent().unwrap() + .parent().unwrap() + .join("_blocks/registries"); + Registry::load_from(&dir).expect("registry load failed") } #[test] - fn no_data_falls_back_to_top_tier() { - let c = fresh_db_with_schema(); - let inp = DecisionInput::new( - "Explore::?::abcd1234::deadbeef-cafef00d", - "find files", - ); - let d = select(&inp, &c).unwrap(); - assert_eq!(d.model, Model::Opus47); - assert_eq!(d.reason, "no_feasible"); + fn pick_default_model_for_code_implementer_rust() { + let r = reg(); + let (prov, model) = pick("code-implementer-rust", &r).unwrap(); + assert_eq!(prov, "anthropic"); + assert_eq!(model, "claude-sonnet-4-6"); } #[test] - fn pinned_short_circuits() { - let c = fresh_db_with_schema(); - let mut inp = DecisionInput::new("any::dna::1234::5678-90ab", "anything"); - inp.pinned = Some(Model::Haiku45); - let d = select(&inp, &c).unwrap(); - assert_eq!(d.model, Model::Haiku45); - assert_eq!(d.reason, "pinned"); + fn pick_codex_reviewer_uses_codex_provider() { + let r = reg(); + let (prov, model) = pick("codex-reviewer", &r).unwrap(); + assert_eq!(prov, "codex"); + assert_eq!(model, "gpt-5-codex"); } #[test] - fn many_haiku_successes_route_to_haiku() { - let c = fresh_db_with_schema(); - // 30 successful Haiku runs on this task class - for i in 0..30 { - c.execute( - "INSERT INTO agents VALUES (?1, 'tc1', 'haiku', 'functional', 0)", - rusqlite::params![format!("a{i}")], - ) - .unwrap(); - } - let mut inp = DecisionInput::new( - "tc1-a-b1234567", - "do the thing", - ); - // make full_dna's task_class_dna = "tc1" - inp.full_dna = "tc1-deadbeef".to_string(); - let d = select(&inp, &c).unwrap(); - assert_eq!(d.model, Model::Haiku45); - assert!(d.quality_lower_bound > 0.70); - } - - #[test] - fn cost_minimization_picks_cheapest_among_feasible() { - let c = fresh_db_with_schema(); - // All three models have plenty of successes - for m in &["haiku", "sonnet", "opus"] { - for i in 0..30 { - c.execute( - "INSERT INTO agents VALUES (?1, 'tc-shared', ?2, 'functional', 0)", - rusqlite::params![format!("{m}{i}"), m], - ) - .unwrap(); - } - } - let mut inp = DecisionInput::new("tc-shared-deadbeef", "anything"); - inp.full_dna = "tc-shared-deadbeef".to_string(); - let d = select(&inp, &c).unwrap(); - assert_eq!(d.model, Model::Haiku45); - assert_eq!(d.reason, "argmin_cost_feasible"); + fn pick_unknown_profile_returns_none() { + let r = reg(); + assert!(pick("does-not-exist", &r).is_none()); } } diff --git a/_primitives/_rust/kei-model-router/src/select_kernel.rs b/_primitives/_rust/kei-model-router/src/select_kernel.rs new file mode 100644 index 0000000..62f91d7 --- /dev/null +++ b/_primitives/_rust/kei-model-router/src/select_kernel.rs @@ -0,0 +1,74 @@ +//! Kernel-smoothed posterior fallback for the empirical selector. +//! +//! When a task-class has no direct ledger entries, borrows posterior mass +//! from neighbouring task-classes weighted by DNA similarity. +//! +//! Constructor Pattern: SQL cube — separated from select.rs to keep both files <200 LOC. + +use crate::kernel::{self, KernelWeights}; +use crate::posterior::Posterior; +use crate::pricing::Model; +use rusqlite::{Connection, Result as SqlResult}; + +const QUERY: &str = "SELECT task_class_dna, + SUM(CASE WHEN outcome = 'functional' + AND COALESCE(escalation_depth, 0) = 0 + THEN 1 ELSE 0 END) AS np, + SUM(CASE WHEN outcome IS NOT NULL + AND NOT (outcome = 'functional' + AND COALESCE(escalation_depth, 0) = 0) + THEN 1 ELSE 0 END) AS nm + FROM agents + WHERE task_class_dna IS NOT NULL + AND task_class_dna != ?1 + AND model = ?2 + GROUP BY task_class_dna"; + +/// Weighted-sum posterior borrowing from neighbour task-classes. +/// +/// Returns a Beta posterior with `alpha`/`beta` inflated by kernel similarity. +/// Starts from a uniform prior (alpha=1, beta=1) and accumulates evidence. +pub fn smooth( + conn: &Connection, + target_task_class: &str, + model: Model, + weights: KernelWeights, +) -> SqlResult { + let mut stmt = conn.prepare(QUERY)?; + + let rows = stmt.query_map( + rusqlite::params![target_task_class, model.slug()], + |r| { + Ok(( + r.get::<_, String>(0)?, + r.get::<_, Option>(1)?.unwrap_or(0), + r.get::<_, Option>(2)?.unwrap_or(0), + )) + }, + )?; + + accumulate_weighted(rows, target_task_class, weights) +} + +fn accumulate_weighted( + rows: impl Iterator>, + target: &str, + weights: KernelWeights, +) -> SqlResult { + let mut alpha = 1.0_f64; + let mut beta = 1.0_f64; + let mut n = 0_u32; + + for row in rows { + let (other_tc, np, nm) = row?; + let sim = kernel::similarity(target, &other_tc, weights); + if sim <= 0.0 { + continue; + } + alpha += sim * np as f64; + beta += sim * nm as f64; + n = n.saturating_add((np + nm) as u32); + } + + Ok(Posterior { alpha, beta, n }) +} diff --git a/_primitives/_rust/kei-model-router/src/select_posterior.rs b/_primitives/_rust/kei-model-router/src/select_posterior.rs new file mode 100644 index 0000000..ae912b8 --- /dev/null +++ b/_primitives/_rust/kei-model-router/src/select_posterior.rs @@ -0,0 +1,178 @@ +//! Empirical-posterior argmin-cost selector. +//! +//! Entry point: `select(input, conn) -> SqlResult`. +//! Reads the ledger, applies kernel smoothing for unseen task-classes, +//! then picks the cheapest model whose quality lower-bound exceeds the threshold. +//! +//! Constructor Pattern: separated from `select.rs` (pick + types) to keep +//! both cubes under 200 LOC. + +use crate::complexity::{self, ComplexityEstimate}; +use crate::dna_class; +use crate::posterior::Posterior; +use crate::pricing::Model; +use crate::select::{Decision, DecisionInput}; +use crate::select_kernel; +use rusqlite::{Connection, Result as SqlResult}; + +pub fn select(input: &DecisionInput, conn: &Connection) -> SqlResult { + let role = dna_class::role(&input.full_dna); + let complexity = complexity::estimate(&input.prompt, role); + + if let Some(m) = input.pinned { + return Ok(pinned_decision(input, complexity, m)); + } + + let task_class = match dna_class::task_class_dna(&input.full_dna) { + Some(t) => t.to_string(), + None => return Ok(fallback_decision(input, complexity, "empty_dna")), + }; + + let feasible = collect_feasible(conn, input, &task_class)?; + if feasible.is_empty() { + return Ok(fallback_decision(input, complexity, "no_feasible")); + } + + let (model, post, lb, cost) = feasible[0]; + Ok(Decision { + model, + expected_cost_micro_cents: cost, + quality_lower_bound: lb, + posterior_n: post.n, + complexity, + reason: "argmin_cost_feasible", + }) +} + +fn collect_feasible( + conn: &Connection, + input: &DecisionInput, + task_class: &str, +) -> SqlResult> { + let mut feasible: Vec<(Model, Posterior, f64, u64)> = Vec::new(); + for m in Model::all() { + let post = posterior_for(conn, task_class, m, input)?; + let lb = post.quality_lower_bound(input.delta); + if lb >= input.q_threshold { + feasible.push((m, post, lb, estimated_cost(input, m))); + } + } + feasible.sort_by_key(|(_, _, _, c)| *c); + Ok(feasible) +} + +fn posterior_for( + conn: &Connection, + task_class: &str, + m: Model, + input: &DecisionInput, +) -> SqlResult { + let post = Posterior::from_ledger(conn, task_class, m)?; + if post.n == 0 { + select_kernel::smooth(conn, task_class, m, input.kernel_weights) + } else { + Ok(post) + } +} + +fn estimated_cost(input: &DecisionInput, m: Model) -> u64 { + let t_in = input.tokens_in.unwrap_or(DecisionInput::DEFAULT_TOKENS_IN); + let t_out = input.tokens_out.unwrap_or(DecisionInput::DEFAULT_TOKENS_OUT); + // Constants mirror models.toml exactly (verified 2026-04-30). + let (in_micro, out_micro): (u64, u64) = match m { + Model::Haiku45 => (100_000_000, 500_000_000), + Model::Sonnet46 => (300_000_000, 1_500_000_000), + Model::Opus47 => (500_000_000, 2_500_000_000), + }; + t_in.saturating_mul(in_micro) / 1_000_000 + + t_out.saturating_mul(out_micro) / 1_000_000 +} + +fn pinned_decision(input: &DecisionInput, complexity: ComplexityEstimate, m: Model) -> Decision { + Decision { + model: m, + expected_cost_micro_cents: estimated_cost(input, m), + quality_lower_bound: 1.0, + posterior_n: 0, + complexity, + reason: "pinned", + } +} + +fn fallback_decision( + input: &DecisionInput, + complexity: ComplexityEstimate, + reason: &'static str, +) -> Decision { + Decision { + model: input.fallback, + expected_cost_micro_cents: estimated_cost(input, input.fallback), + quality_lower_bound: 0.0, + posterior_n: 0, + complexity, + reason, + } +} + +// ────────────────────────────────────────────────────────────────────────────── +// Tests +// ────────────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::pricing::Model; + use crate::select::DecisionInput; + use rusqlite::Connection; + + fn fresh_db() -> Connection { + let c = Connection::open_in_memory().unwrap(); + c.execute_batch( + "CREATE TABLE agents ( + id TEXT, task_class_dna TEXT, model TEXT, + outcome TEXT, escalation_depth INTEGER DEFAULT 0 + );", + ) + .unwrap(); + c + } + + #[test] + fn no_data_falls_back_to_top_tier() { + let c = fresh_db(); + let inp = DecisionInput::new( + "Explore::?::abcd1234::deadbeef-cafef00d", + "find files", + ); + let d = select(&inp, &c).unwrap(); + assert_eq!(d.model, Model::Opus47); + assert_eq!(d.reason, "no_feasible"); + } + + #[test] + fn pinned_short_circuits() { + let c = fresh_db(); + let mut inp = DecisionInput::new("any::dna::1234::5678-90ab", "anything"); + inp.pinned = Some(Model::Haiku45); + let d = select(&inp, &c).unwrap(); + assert_eq!(d.model, Model::Haiku45); + assert_eq!(d.reason, "pinned"); + } + + #[test] + fn many_haiku_successes_route_to_haiku() { + let c = fresh_db(); + for i in 0..30 { + c.execute( + "INSERT INTO agents VALUES (?1,'tc1','claude-haiku-4-5','functional',0)", + rusqlite::params![format!("a{i}")], + ) + .unwrap(); + } + let mut inp = DecisionInput::new("tc1-deadbeef", "do the thing"); + inp.full_dna = "tc1-deadbeef".to_string(); + let d = select(&inp, &c).unwrap(); + assert_eq!(d.model, Model::Haiku45); + assert!(d.quality_lower_bound > 0.70); + } +} diff --git a/docs/DNA-INDEX.md b/docs/DNA-INDEX.md index a71bad7..bf0ed71 100644 --- a/docs/DNA-INDEX.md +++ b/docs/DNA-INDEX.md @@ -1,20 +1,20 @@ # KeiSeiKit DNA Encyclopedia -> Auto-generated from kei-registry. Last regenerated: 2026-05-12T13:17:58Z. -> Total blocks: 672. Per-type breakdown: +> Auto-generated from kei-registry. Last regenerated: 2026-05-13T13:05:08Z. +> Total blocks: 679. Per-type breakdown: | Type | Count | |---|---:| -| atom | 149 | +| atom | 150 | | hook | 74 | | manifest | 38 | -| primitive | 144 | +| primitive | 150 | | rule | 183 | | skill | 84 | --- -## Primitive (144) +## Primitive (150) Sorted alphabetically by name. @@ -29,7 +29,7 @@ Sorted alphabetically by name. | kei-arch-map | primitive::cli,hash,… | _primitives/_rust/kei-arch-map/Cargo.toml | e87846b9156e06d3 | | kei-arch-map::kei-arch-map | primitive::_::7b2994… | _primitives/_rust/kei-arch-map/Cargo.toml | 6ac9819e | | kei-artifact | primitive::cli,hash,… | _primitives/_rust/kei-artifact/Cargo.toml | fa5827db205a9c89 | -| kei-atom-discovery | primitive::fs,md::85… | _primitives/_rust/kei-atom-discovery/Cargo.toml | f8fc6fba7b2bd67f | +| kei-atom-discovery | primitive::fs,md::85… | _primitives/_rust/kei-atom-discovery/Cargo.toml | f88f3d251d6ba9bc | | kei-auth | primitive::cli,hash,… | _primitives/_rust/kei-auth/Cargo.toml | 1de101b34ebd0522 | | kei-auth-apple | primitive::hash,md,n… | _primitives/_rust/kei-auth-apple/Cargo.toml | c0bcbfa5dc613137 | | kei-auth-google | primitive::hash,md,n… | _primitives/_rust/kei-auth-google/Cargo.toml | a8b9ff9fed67bf5b | @@ -37,6 +37,7 @@ Sorted alphabetically by name. | kei-auth-webauthn | primitive::md,networ… | _primitives/_rust/kei-auth-webauthn/Cargo.toml | b023f2ab40e7e9bf | | kei-backend-daytona | primitive::md,networ… | _primitives/_rust/kei-backend-daytona/Cargo.toml | c7566eedb7ff14a9 | | kei-brain-view | primitive::cli,md,sq… | _primitives/_rust/kei-brain-view/Cargo.toml | 4969c1a066ef413e | +| kei-buddy | primitive::cli,md,ne… | _primitives/_rust/kei-buddy/Cargo.toml | 1d981880363984a2 | | kei-cache | primitive::cli,hash,… | _primitives/_rust/kei-cache/Cargo.toml | 1d0db22246a5978b | | kei-cache::kei-cache | primitive::_::db2dbd… | _primitives/_rust/kei-cache/Cargo.toml | 78cc768a | | kei-capability | primitive::cli,md::d… | _primitives/_rust/kei-capability/Cargo.toml | 3bcaea4da8ce41da | @@ -51,8 +52,10 @@ Sorted alphabetically by name. | kei-compute-linode | primitive::cli,md,ne… | _primitives/_rust/kei-compute-linode/Cargo.toml | a2c366d4d0003d68 | | kei-compute-vultr | primitive::cli,md,ne… | _primitives/_rust/kei-compute-vultr/Cargo.toml | d8c523ddf97a6a17 | | kei-conflict-scan | primitive::cli,fs,md… | _primitives/_rust/kei-conflict-scan/Cargo.toml | a6d3571490ba4d6c | +| kei-contacts-apple | primitive::md,networ… | _primitives/_rust/kei-contacts-apple/Cargo.toml | a8185e72656d424b | +| kei-contacts-google | primitive::md,networ… | _primitives/_rust/kei-contacts-google/Cargo.toml | 4ab1268b561a4084 | | kei-content-store | primitive::cli,hash,… | _primitives/_rust/kei-content-store/Cargo.toml | b9523105a6561601 | -| kei-cortex | primitive::cli,fs,md… | _primitives/_rust/kei-cortex/Cargo.toml | d91652e65cf4e52a | +| kei-cortex | primitive::cli,fs,md… | _primitives/_rust/kei-cortex/Cargo.toml | 933fe1cb1b2fb522 | | kei-cortex::kei-cortex | primitive::_::215cd1… | _primitives/_rust/kei-cortex/Cargo.toml | 6bc05e60 | | kei-cron-scheduler | primitive::md,networ… | _primitives/_rust/kei-cron-scheduler/Cargo.toml | 01d1daef49c3a38c | | kei-crossdomain | primitive::cli,md,sq… | _primitives/_rust/kei-crossdomain/Cargo.toml | ae582e4ca8c58339 | @@ -116,12 +119,12 @@ Sorted alphabetically by name. | kei-pipeline-test | primitive::_::856b77… | _primitives/_rust/kei-pipeline-test/Cargo.toml | 45ff17c5a735e751 | | kei-pipeline-test::kei-pipeline-test | primitive::_::d57c1d… | _primitives/_rust/kei-pipeline-test/Cargo.toml | 08ac0613 | | kei-projects-index | primitive::cli,fs,md… | _primitives/_rust/kei-projects-index/Cargo.toml | fef5af180ea88a89 | -| kei-projects-watcher | primitive::cli,md,ne… | _primitives/_rust/kei-projects-watcher/Cargo.toml | 738638606d5e8d16 | +| kei-projects-watcher | primitive::cli,md,ne… | _primitives/_rust/kei-projects-watcher/Cargo.toml | 8aaecb2a171f202b | | kei-provision | primitive::cli,md::1… | _primitives/_rust/kei-provision/Cargo.toml | d1ae29e76a9b3275 | | kei-provision::kei-provision | primitive::_::46c768… | _primitives/_rust/kei-provision/Cargo.toml | f8463bde | | kei-prune | primitive::cli,md,sq… | _primitives/_rust/kei-prune/Cargo.toml | 912fa6e551df94d6 | | kei-refactor-engine | primitive::cli,md::c… | _primitives/_rust/kei-refactor-engine/Cargo.toml | 55447926330313be | -| kei-registry | primitive::cli,fs,ha… | _primitives/_rust/kei-registry/Cargo.toml | f5fc71fe14c1500f | +| kei-registry | primitive::cli,fs,ha… | _primitives/_rust/kei-registry/Cargo.toml | 52423c8cca6fcc56 | | kei-registry::foo | primitive::_::12366c… | _primitives/_rust/kei-registry/tests/fixtures/fake-kit/_primitives/_rust/foo/Cargo.toml | 403bc4b0 | | kei-registry::foo | primitive::_::3937fa… | _primitives/_rust/kei-registry/tests/fixtures/fake-kit/_primitives/_rust/foo/Cargo.toml | 403bc4b0 | | kei-registry::foo | primitive::_::908700… | _primitives/_rust/kei-registry/tests/fixtures/fake-kit/_primitives/_rust/foo/Cargo.toml | 403bc4b0 | @@ -141,20 +144,23 @@ Sorted alphabetically by name. | kei-router::kei-router | primitive::_::b629c4… | _primitives/_rust/kei-router/Cargo.toml | b46c86d0 | | kei-runtime | primitive::cli,fs,md… | _primitives/_rust/kei-runtime/Cargo.toml | 0b1c71146c683dd7 | | kei-runtime-core | primitive::hash,md,n… | _primitives/_rust/kei-runtime-core/Cargo.toml | 3ec878e2dd71176a | -| kei-sage | primitive::cli,fs,md… | _primitives/_rust/kei-sage/Cargo.toml | 443fcc309d0cbaa1 | +| kei-sage | primitive::cli,fs,md… | _primitives/_rust/kei-sage/Cargo.toml | d1c7d2811c3b132d | | kei-scheduler | primitive::cli,md,sq… | _primitives/_rust/kei-scheduler/Cargo.toml | 71e428667c0a51de | | kei-search-core | primitive::cli,md,sq… | _primitives/_rust/kei-search-core/Cargo.toml | 4414782368af2908 | | kei-shared | primitive::md::9db37… | _primitives/_rust/kei-shared/Cargo.toml | 881038bdfa81b0a8 | -| kei-skill-importer | primitive::cli,fs,md… | _primitives/_rust/kei-skill-importer/Cargo.toml | 9a8f8225093a7ce6 | +| kei-skill-importer | primitive::cli,fs,md… | _primitives/_rust/kei-skill-importer/Cargo.toml | 96a13747f9a93260 | | kei-skills | primitive::fs,md,reg… | _primitives/_rust/kei-skills/Cargo.toml | 168eae705265c03a | | kei-social-store | primitive::cli,md,sq… | _primitives/_rust/kei-social-store/Cargo.toml | 4ec4ddcde6a7d07b | | kei-spawn | primitive::cli,hash,… | _primitives/_rust/kei-spawn/Cargo.toml | 11e0329ce919b898 | | kei-store | primitive::cli,md,ne… | _primitives/_rust/kei-store/Cargo.toml | 8577af6c0d58ce9d | +| kei-stt | primitive::md,networ… | _primitives/_rust/kei-stt/Cargo.toml | 995b68520a968d8c | | kei-substrate-types | primitive::md::47dea… | _primitives/_rust/kei-substrate-types/Cargo.toml | 27e498f01091f17b | | kei-svc-systemd | primitive::cli,md,ne… | _primitives/_rust/kei-svc-systemd/Cargo.toml | 8f85fbec44996ade | | kei-task | primitive::cli,md,sq… | _primitives/_rust/kei-task/Cargo.toml | 127047cf636088f2 | +| kei-telegram-webhook | primitive::md,networ… | _primitives/_rust/kei-telegram-webhook/Cargo.toml | 0d746aaa951b2d2d | | kei-tlog | primitive::md::9efee… | _primitives/_rust/kei-tlog/Cargo.toml | 5a2820a3b829a4be | | kei-token-tracker | primitive::cli,md,sq… | _primitives/_rust/kei-token-tracker/Cargo.toml | b7f429845eec3ce2 | +| kei-tts | primitive::md,networ… | _primitives/_rust/kei-tts/Cargo.toml | fbec46e9c6221a7a | | kei-tty | primitive::cli,md,ne… | _primitives/_rust/kei-tty/Cargo.toml | 8b2c89af074f79de | | kei-watch | primitive::cli,md::2… | _primitives/_rust/kei-watch/Cargo.toml | 1de6e250bbf8c82d | | keidna-sign | primitive::cli,fs,ha… | _primitives/_rust/keidna-sign/Cargo.toml | b6d5f10993eaa4db | @@ -194,8 +200,8 @@ Sorted alphabetically by name. | /wave-audit — 3-Wave Parallel Audit | md | skill::md::3c0b33a5c… | skills/wave-audit/SKILL.md | | /wave-audit — 3-Wave Parallel Audit | md | skill::md::150f84799… | skills/wave-audit/SKILL.md | | 3D Scene Skill | md | skill::md::53fc17a07… | skills/3d-scene/SKILL.md | -| AI Animation Pipeline | md | skill::md::71529ec4:… | skills/ai-animation/skill.md | | AI Animation Pipeline | md | skill::md::5102577d3… | skills/ai-animation/SKILL.md | +| AI Animation Pipeline | md | skill::md::71529ec40… | skills/ai-animation/skill.md | | API-Design — Style, Contract & Lifecycle Pipeline (index) | md | skill::md::85d94768d… | skills/api-design/SKILL.md | | Accessibility Audit — WCAG 2.2 AA | md | skill::md::be686747b… | skills/a11y-audit/SKILL.md | | Architecture Rules Engine | md | skill::md::8d2151f68… | skills/architecture-rules/SKILL.md | @@ -233,8 +239,8 @@ Sorted alphabetically by name. | Performance Audit Workflow | md | skill::md::dfd2bf23b… | skills/perf-audit/SKILL.md | | Pet Init — Interactive Persona Wizard (index) | md | skill::md::4f793fef7… | skills/pet-init/SKILL.md | | Quick API Scaffold Workflow | md | skill::md::645d8159f… | skills/quick-api/SKILL.md | -| RAG Pipeline Skill | md | skill::md::b62e8900:… | skills/rag-pipeline/skill.md | | RAG Pipeline Skill | md | skill::md::d1ef17764… | skills/rag-pipeline/SKILL.md | +| RAG Pipeline Skill | md | skill::md::b62e8900b… | skills/rag-pipeline/skill.md | | Refactor Workflow | md | skill::md::7669f25fd… | skills/refactor/SKILL.md | | Responsive Audit Workflow | md | skill::md::ff87607a8… | skills/responsive-audit/SKILL.md | | SEO Audit Workflow | md | skill::md::a3be7db51… | skills/seo-audit/SKILL.md | @@ -985,7 +991,7 @@ Sorted alphabetically by name. | tomd-preread | shell | hook::shell::8a95b76… | hooks/tomd-preread.sh | | tool-use-event | shell | hook::shell::34bb788… | hooks/tool-use-event.sh | -## Atom (149) +## Atom (150) Sorted alphabetically by name. @@ -1004,6 +1010,7 @@ Sorted alphabetically by name. | AUTH — Passkeys (WebAuthn / FIDO2) | atom::_::94c5d302293… | _blocks/auth-passkeys.md | 97eefc78cb030bff | | AUTH — Sessions & Cookies (+JWT tradeoff) | atom::_::a11a36d9846… | _blocks/auth-sessions.md | f3359b91d153fd53 | | BASELINE — inherit from Main Claude (never violate) | atom::_::477f2902b64… | _blocks/baseline.md | 44fc4025352bb55c | +| Block: multi-critic-fresh-context | atom::_::310c7935abe… | _blocks/multi-critic-fresh-context.md | 58bb6f2216667dee | | CI — Forgejo Actions (self-hosted, Tailscale-only admin) | atom::_::225f31003a0… | _blocks/ci-forgejo-actions.md | f2ac5ad0223d2759 | | CI — GitHub Actions (OIDC, matrix, cache, reusable workflows) | atom::_::032b667bc24… | _blocks/ci-github-actions.md | ba80d3dfe2d1c970 | | CI — Release automation (SemVer, changelog, tagging) | atom::_::c42ae6cfe7d… | _blocks/ci-release-automation.md | 99ad09c3e9a674f5 | @@ -1073,7 +1080,7 @@ Sorted alphabetically by name. | TEST — Property-based testing (invariants + shrinking) | atom::_::d2c8bd9e3de… | _blocks/test-property.md | 329287abaf343562 | | TEST-FIRST | atom::_::2158b9334db… | _blocks/rule-test-first.md | b65a0c3a371f2f2d | | `_blocks/` — Composable Agent Content | atom::_::c81449903b7… | _blocks/README.md | bd6e19eec320c6b7 | -| auditor | atom::_::b46e86dbba4… | _roles/auditor.toml | 2a02d2ee7ee88e35 | +| auditor | atom::_::b46e86dbba4… | _roles/auditor.toml | 74d9689ef7d3724e | | edit-local | atom::_::b7724e4f3aa… | _roles/edit-local.toml | 35ca99714901df66 | | edit-shared | atom::_::db022330517… | _roles/edit-shared.toml | 332b1a8b0323fb60 | | explorer | atom::_::892af91242a… | _roles/explorer.toml | e852f2dfbb7b058f | @@ -1161,10 +1168,10 @@ Sorted alphabetically by name. | critic-bug | manifest::_::0272455… | _manifests/critic-bug.toml | c3ec88f25871296f | | critic-perf | manifest::_::b50a6be… | _manifests/critic-perf.toml | 0fb071fa7eaab564 | | critic-tech-debt | manifest::_::b3d6e89… | _manifests/critic-tech-debt.toml | af98047e524fb2bf | -| fal-ai-runner | manifest::_::7a7c8e2… | _manifests/fal-ai-runner.toml | f287fb80f3ed590b | +| fal-ai-runner | manifest::_::7a7c8e2… | _manifests/fal-ai-runner.toml | c03c6ce7ed52b6d4 | | frontend-validator | manifest::_::1c3447f… | _manifests/frontend-validator.toml | 2a27cb166cad8eb0 | | infra-implementer | manifest::_::94c8642… | _manifests/infra-implementer.toml | 37ce7a2d1f858a78 | -| infra-implementer-cicd | manifest::_::6465024… | _manifests/infra-implementer-cicd.toml | db066df6fc855524 | +| infra-implementer-cicd | manifest::_::6465024… | _manifests/infra-implementer-cicd.toml | 5ba585df9fc0695a | | infra-implementer-container | manifest::_::38f9d49… | _manifests/infra-implementer-container.toml | b069db59d93de252 | | infra-implementer-iac | manifest::_::ff44de8… | _manifests/infra-implementer-iac.toml | 7d94f117498c8977 | | infra-implementer-secrets | manifest::_::66a7ec5… | _manifests/infra-implementer-secrets.toml | ec3bb16a335f699c | @@ -1205,7 +1212,7 @@ Sorted alphabetically by name. - `/vm-provision — 6-Phase VPS Pipeline (index)` — 3 versions: c3cdf6f2 → 04a5eb35 → 04a5eb35731ad538 - `/wave-audit — 3-Wave Parallel Audit` — 59 versions: b754cd05 → 01329795 → 01329795ba38a5ac → e116e138cfd55f93 → 7c1de001814a0376 → e25ae7688e7919c4 → 7b59e0c07d7ac394 → 090a78c36e43332a → f0a99365d19e3466 → 40ab6805f2c896bc → 4351790b948a3fbd → 199363cb616e51c5 → 0d2ef02b30098a65 → d13433cc75b3fd95 → 11f4ce5905bb2b08 → 5a7e01aaf9d49ceb → 55d88592f1f6d7da → e5625030dc7a18c6 → e995eb849341e001 → f7110fb0fb39d075 → c8d6aab5d7a55cdc → 5be3ca67d7708668 → 12933bb2852bd7d4 → c33338f03eca80bb → 2317084ca9929d7a → 67c686ebae79aece → 3789d7dedd1e2a9a → 8b72264b418ba989 → 32e16959f50a5688 → 22b2c8216dc6e6ad → 7a9ee89e2682f809 → 9c8345ae3276b783 → 8d8ea38ef0d7676a → 0bf0bae20c44fef0 → b62735250b9d9848 → 52b2a882fbc55430 → c48eadcfab7bf0a5 → 0e820ee2d3c70feb → f5c142793c66def0 → fd904c6ba5f3f9b8 → 138997cb014305ec → 23a8b6fc03d35529 → 616ae6ea95422445 → 5ee3de4d82a91d2f → 23f4603922e5cf95 → c37544ac08e7fc57 → 112af96feca608d6 → 6ffce790dbadf446 → 18b99e8cc22ac6a6 → 6db8e96db777fdc4 → 6e901dc26054e973 → c252f2d53f44f820 → b0e2767a721d7c74 → 82954089d94488a5 → cc882ffb7fa6fb1c → 3f241a501aa6d477 → cc6630c7f24b0dab → d71f4baf36f0378d → 90e52d2532482010 - `3D Scene Skill` — 4 versions: e31a87ca → ca06fcac → e31a87ca → e31a87caffc57858 -- `AI Animation Pipeline` — 5 versions: 7c4b005c → 92865368 → 92865368 → 92865368cc0fcb0e → 7c4b005cd70d24f3 +- `AI Animation Pipeline` — 6 versions: 7c4b005c → 92865368 → 92865368 → 92865368cc0fcb0e → 7c4b005cd70d24f3 → 92865368cc0fcb0e - `API — Anthropic (Claude)` — 2 versions: 4cba1946 → 4cba19469d0a9037 - `API — Apify (web scraping platform)` — 2 versions: f7c27f78 → f7c27f788592c0fc - `API — ElevenLabs (voice)` — 2 versions: 458d19af → 458d19af84101d83 @@ -1295,7 +1302,7 @@ Sorted alphabetically by name. - `Pipeline 5-Phase Wizard Template (shared preamble)` — 2 versions: 8eca71b8 → 8eca71b8d473ab01 - `Pure-Click Contract` — 2 versions: 9fdb2d9a → 9fdb2d9a6d8569b0 - `Quick API Scaffold Workflow` — 2 versions: 78055aeb → 78055aebfc0fae07 -- `RAG Pipeline Skill` — 4 versions: 11c73aca → e47cc310 → e47cc31042cb0afd → a5b3e02da3c62374 +- `RAG Pipeline Skill` — 5 versions: 11c73aca → e47cc310 → e47cc31042cb0afd → a5b3e02da3c62374 → e47cc31042cb0afd - `Refactor Workflow` — 3 versions: aab43956 → 0c0163b1 → 0c0163b140b69921 - `Responsive Audit Workflow` — 2 versions: c1b0f673 → c1b0f6735a67cadd - `SECURITY — Audit Logging (auditd + journald forwarding)` — 2 versions: 3bafc6f8 → 3bafc6f89a817904 @@ -1348,21 +1355,21 @@ Sorted alphabetically by name. - `agent-fork-logger` — 2 versions: be6de747 → be6de747443f2744 - `agent-heartbeat-tick` — 3 versions: 5eb00dc3 → 560fa0f8 → 560fa0f8578d5b17 - `agent-outcome-backfill` — 4 versions: 0e00d9ca → c901aaf2 → a11281aa → a11281aabfc7f783 -- `agent-stub-scan` — 9 versions: 8a9fc155 → 4098a307 → 3888d5eb → d792e3ba → fd655f66 → 173885ea → 173885ea → 173885eaef0eb8b2 → 5471a80acac812a2 +- `agent-stub-scan` — 10 versions: 8a9fc155 → 4098a307 → 3888d5eb → d792e3ba → fd655f66 → 173885ea → 173885ea → 173885eaef0eb8b2 → 5471a80acac812a2 → 4098a3073bb1a097 - `alignment-check` — 5 versions: 4e7389b1 → b1e18549 → 31600957 → 31600957955596aa → 15cc6686ccf20148 - `arch-verify-precommit` — 9 versions: 1b4149b0 → e9d1ea43 → d021ce1b → 7db0b5c5 → 87ba9181 → 27be57da → 67740b61 → 0fab51c2 → 0fab51c21f7ae356 - `arch-verify-precommit.test` — 3 versions: 268e824a → 4c5ccc9e → 4c5ccc9e3278757c - `assemble-agents` — 2 versions: 5b6c105a → 5b6c105a42bc5046 - `assemble-validate` — 2 versions: ef681f01 → ef681f01161e7d5c -- `auditor` — 4 versions: 7eb6ab3a → 74d9689e → 2a02d2ee → 2a02d2ee7ee88e35 +- `auditor` — 5 versions: 7eb6ab3a → 74d9689e → 2a02d2ee → 2a02d2ee7ee88e35 → 74d9689ef7d3724e - `auto-dev-guard` — 2 versions: c21b1488 → c21b14883f71c9b2 - `auto-encyclopedia-refresh` — 2 versions: f06633d5 → f06633d50f530240 - `auto-register-on-edit` — 2 versions: cde1da42 → cde1da42f9d9054b -- `block-dangerous` — 4 versions: c4aea975 → d479220b → d479220b486d0016 → b5e472bb8e39626e +- `block-dangerous` — 5 versions: c4aea975 → d479220b → d479220b486d0016 → b5e472bb8e39626e → d479220b486d0016 - `chat-numeric-postflag` — 8 versions: 5227cdfe → 5227cdfe → 5227cdfe047f4c13 → 856b55c725844b9f → dbd4fd908df47391 → 150f1df8ac226bbf → c30e5ee256a39d40 → c4d8a87a21686c0e - `chat-numeric-prewarn` — 4 versions: 36f9f4f7 → 36f9f4f7 → 36f9f4f7692a024c → bf606f7aca5e44f9 -- `check-error-patterns` — 2 versions: de2866b5 → be07f0de -- `citation-verify` — 4 versions: e65d32af → c7d4715f → c499c45d → c499c45dff0cacba +- `check-error-patterns` — 3 versions: de2866b5 → be07f0de → be07f0de0816842d +- `citation-verify` — 5 versions: e65d32af → c7d4715f → c499c45d → c499c45dff0cacba → c7d4715f99bedce1 - `decompose-rules-on-edit` — 2 versions: 7782a607 → 7782a607e4a72245 - `destructive-guard` — 3 versions: 80c352e6 → a329d569 → a329d56980bb40c5 - `disk-headroom-check` — 6 versions: 77571e2d → 77571e2d34b01325 → f82d7c87a4ecf590 → b16819dcc8098825 → e53b50787ab8baac → eaebde6b565e82b8 @@ -1372,12 +1379,14 @@ Sorted alphabetically by name. - `error-spike-detector` — 2 versions: 83f44d39 → 83f44d3963bd26fa - `explorer` — 3 versions: d61c4f89 → e852f2df → e852f2dfbb7b058f - `extract-task-durations` — 3 versions: e6854ef5 → 859873eb → 859873eb37fe23bb +- `fal-ai-runner` — 2 versions: f287fb80f3ed590b → c03c6ce7ed52b6d4 - `firewall-diff` — 4 versions: e42f1e32 → 8260ffc0 → 48baaf6f → 48baaf6f8e0dd928 - `foo` — 19 versions: 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fa → 309b88fade16aa73 - `frustration-matrix` — 5 versions: 0923b30a → d51e63c8 → 4df8a04e → ee43fbc9 → ee43fbc92cb31ff0 - `frustration-matrix::frustration-matrix` — 8 versions: db99150c → db99150c → 8f319334 → 0968042d → 8b155505 → 7294d811 → 95104457 → 1b1cd725 - `git-ops` — 2 versions: da80a8e7 → da80a8e74bb706e6 - `graph-export-watcher` — 2 versions: 11bf6653 → 11bf6653db19386c +- `infra-implementer-cicd` — 2 versions: db066df6fc855524 → 5ba585df9fc0695a - `kei-agent-runtime` — 5 versions: 708830d4 → 33b44d6c → 841ac805 → f1218935 → f121893581449fef - `kei-agent-runtime::kei-agent-runtime` — 6 versions: 76e04f24 → 76e04f24 → f33a7022 → 45500a16 → 3e5d1243 → 8a05daf6 - `kei-arch-derive` — 2 versions: 554bf8d6 → 554bf8d655112fe5 @@ -1386,7 +1395,7 @@ Sorted alphabetically by name. - `kei-arch-map::kei-arch-map` — 90 versions: 2e9d962a → 8f857390 → 31c4476e → a5a88192 → 56108075 → 489c0d17 → 0249bfe4 → 33cddca1 → 9fda4ce7 → 6dbc8cc7 → d6438878 → 2389b369 → aac0b7e2 → 3dd66c1b → 43d00213 → a78aab5e → b26c1553 → 288a06ff → c0af043e → 00bfa19d → 67dae440 → 6b450504 → 631c4f6d → abac7b08 → b9b2ae96 → 4021c4ef → 26742798 → 05e55a4d → 07a38bc2 → 2641fb3f → acfac7a8 → b6a985d1 → 616d676f → 83596ed7 → 19db5b14 → f9cc92dd → 12f810ca → cf0e7d83 → 8b4d9c93 → e21c155b → b149f5a3 → 5d343463 → 1bc51349 → 2f4ae1e3 → a0991b1c → 5c1b60be → 312c7233 → bf8d22c7 → ec790973 → d276a710 → 9c87971c → 38d7a017 → 2e9d962a → 2c19c2ba → fd84bfe4 → 7c564024 → b07c49a5 → 0b6bc47a → a40e7bab → 4703d4d7 → 9f0da613 → a89aa071 → 65ddc8e2 → fa7973c2 → ce6436c9 → 8e3a6d78 → 808f3fb6 → 88a40957 → de7e5352 → c13aa048 → b64f65f9 → c0a9abac → 3028b210 → 6ac9819e → 101ce920 → 1e4634ae → 2f740fba → 39cf8d48 → 9abd7954 → 653a93fd → 2062f53a → 86c1025b → 7d1a4fba → f1e85972 → 9cbb6969 → 640ee712 → e8203dad → c883c49f → b1499c38 → 44929e98 - `kei-artifact` — 5 versions: 2c55b84a → a33abf97 → 50e8c9cd → fa5827db → fa5827db205a9c89 - `kei-artifact::kei-artifact` — 2 versions: 8742aade → 8742aade -- `kei-atom-discovery` — 5 versions: 0d532c9f → ca9202b5 → e1fde01b → f8fc6fba → f8fc6fba7b2bd67f +- `kei-atom-discovery` — 6 versions: 0d532c9f → ca9202b5 → e1fde01b → f8fc6fba → f8fc6fba7b2bd67f → f88f3d251d6ba9bc - `kei-atom-discovery::kei-atom-discovery` — 9 versions: bb5db6ab → bb5db6ab → 16cf10b2 → fc1cf213 → 9453e65b → 6e1c3f41 → f9d2532f → 8089e720 → 0cc23991 - `kei-auth` — 5 versions: bb941dd2 → 28e0b700 → 1ecaa9b2 → 1de101b3 → 1de101b34ebd0522 - `kei-auth-apple` — 7 versions: 29ddf78c → 166a2e48 → f005a8c3 → f005a8c3 → fec3df65 → c0bcbfa5 → c0bcbfa5dc613137 @@ -1426,7 +1435,7 @@ Sorted alphabetically by name. - `kei-conflict-scan::kei-conflict-scan` — 2 versions: 6f99b956 → 6f99b956 - `kei-content-store` — 5 versions: 11ed9bd8 → ea462cc4 → b86f6d90 → b9523105 → b9523105a6561601 - `kei-content-store::kei-content-store` — 2 versions: cbcf91b6 → cbcf91b6 -- `kei-cortex` — 8 versions: 4815eb79 → 47d1b6ba → 6e01fa0d → 6e01fa0d → 6e01fa0d → 6e01fa0d → d91652e6 → d91652e65cf4e52a +- `kei-cortex` — 9 versions: 4815eb79 → 47d1b6ba → 6e01fa0d → 6e01fa0d → 6e01fa0d → 6e01fa0d → d91652e6 → d91652e65cf4e52a → 933fe1cb1b2fb522 - `kei-cortex::kei-cortex` — 162 versions: 2305a894 → b046411d → 31e30021 → 0e1fdd58 → ee42ea3c → ea55151c → 5a91990e → 48b55962 → 9d197f44 → 44dcf2b8 → f82717c3 → 6beb14d1 → 7c783b8b → 6f4566d6 → ae6673fb → cb55caac → 0544a125 → 906fe71e → dda08557 → a9d9835c → c6bb1a76 → ff69e910 → 8c2a2cd0 → a4f10ba1 → 3e1d80b9 → a42dc172 → 9d1faba6 → 8c098c2a → ed51e643 → 8e611e78 → b0e5fc42 → d5acba40 → ea37b0a2 → ef485e8b → 4ee863b3 → 7b9b0b84 → b75a06c5 → 154d5906 → ccf3586b → bfa4e51e → 2d4d2abe → 5f7a5fac → ae4e5a1a → 81387a8b → 98f37df7 → 1f8a6a5e → a7910ea4 → bcbb7ede → 44165ca9 → 213f02fc → 2f0a30bd → 72bb72f0 → b5167b4d → b547ea78 → 22fd0a17 → 48c02bd1 → 5dc0ae1b → f92ef035 → d88d40c8 → 304b82c3 → 1aae122b → 2dd97fb1 → 0c0763ba → 3a2dc192 → db0268b2 → 96d4c01e → ad8c681f → 96d4c01e → 42442b7d → 4f866eae → 78f70ea8 → 7f18e568 → 43f90d7d → fa410710 → 875d5a2a → b6203887 → 8ead3163 → e76cddd8 → dd9c9514 → b66b6cba → 4bbaf015 → b58768b5 → b179e553 → 1da94835 → 0da17c6a → e7b4f1b0 → d4db0252 → 01226b1a → 750f5ffd → 1c0a1a8e → d55eb5bd → 87588688 → b4f95eb5 → aee28766 → 29e25e78 → 6275797b → f7c79fb5 → 34de185c → 3028f8a9 → 34de185c → b77a7549 → 7d2685d5 → 189ebf41 → e08cd8fc → 1db22f1c → 76ee9811 → 56bc509b → 64281b3f → 64281b3f → c85180c6 → f8710632 → 473d4a14 → f5eba99f → 7286f776 → 0cf69e53 → 9e7db3d6 → 3f01a64f → e1aad130 → 5a151eea → 72cbb966 → 49aee825 → 09c222a2 → 4b093b08 → 66a4d99c → bd31347a → a5a8695e → 6f302eea → 694bedfe → 10917911 → 531ff7da → 92ecd22c → ebbf0aa2 → e0049936 → 847f19fc → a45c95fc → b5e1f645 → c235781e → a8c8c8e5 → 08b34680 → 774ca445 → 860ac0ae → 1672a684 → eba70cac → 38e09697 → d176d2e1 → 18cd5d2e → 9b912256 → 023155ef → 6d9f2e7e → e4ac74df → 4d1bebb6 → e5fe601f → ea939c2b → 920e783a → 16c80b06 → f99f8951 → 77d680d4 → 6bc05e60 → b3cb07df → 266d3749 → 0c556040 → 2afaa4ca - `kei-cron-scheduler` — 5 versions: da2674f5 → a702296b → e59b51d5 → 01d1daef → 01d1daef49c3a38c - `kei-cron-scheduler::kei-cron-scheduler` — 2 versions: c4c0e774 → c4c0e774 @@ -1514,7 +1523,7 @@ Sorted alphabetically by name. - `kei-pipeline-test::kei-pipeline-test` — 2 versions: 2e9d962a → 08ac0613 - `kei-projects-index` — 5 versions: ce1576f0 → c5ecb5ee → 8e2e7128 → fef5af18 → fef5af180ea88a89 - `kei-projects-index::kei-projects-index` — 2 versions: 809d1c77 → 809d1c77 -- `kei-projects-watcher` — 5 versions: dedc5323 → dd3a3b8c → a9504a37 → 8aaecb2a → 738638606d5e8d16 +- `kei-projects-watcher` — 6 versions: dedc5323 → dd3a3b8c → a9504a37 → 8aaecb2a → 738638606d5e8d16 → 8aaecb2a171f202b - `kei-projects-watcher::kei-projects-watcher` — 17 versions: cd10e92b → cd10e92b → 9608f9ef → bc82263f → 6351f4e0 → a9cb0aa2 → 2c036ff9 → 48e84b56 → 5bdf2837 → a51cd5e8 → 6683b2b4 → adba0a04 → fb61929a → b5e6ed55 → e423b99c → 80566e17 → a5adbe2e - `kei-provision` — 5 versions: 1d613e5d → cfa53bb3 → 86821ebb → d1ae29e7 → d1ae29e76a9b3275 - `kei-provision::kei-provision` — 6 versions: 0ec7cd2f → 0ec7cd2f → b1da9dd4 → 8efb7c7e → 6bb23485 → f8463bde @@ -1522,7 +1531,7 @@ Sorted alphabetically by name. - `kei-prune::kei-prune` — 2 versions: e4b33b11 → e4b33b11 - `kei-refactor-engine` — 5 versions: 90048888 → 92e83ce0 → 01f1f681 → 55447926 → 55447926330313be - `kei-refactor-engine::kei-refactor-engine` — 17 versions: 7d8c5bfb → 7d8c5bfb → 84f68a72 → beda9e61 → 1dde9ffc → 6df9785d → 62f2a855 → 761d1f21 → e25a9173 → 4d34a7f7 → 854124dc → aed7fa84 → 29bddee3 → 4e98da43 → c4b1c6c7 → d1fb4cc4 → 392c9aa7 -- `kei-registry` — 6 versions: 7d9570ad → 5a2e79d8 → 5a2e79d8 → 5a2e79d8 → 52423c8c → f5fc71fe14c1500f +- `kei-registry` — 7 versions: 7d9570ad → 5a2e79d8 → 5a2e79d8 → 5a2e79d8 → 52423c8c → f5fc71fe14c1500f → 52423c8cca6fcc56 - `kei-registry::foo` — 5 versions: 403bc4b0 → 403bc4b0 → 403bc4b0 → 403bc4b0 → 403bc4b0 - `kei-registry::kei-registry` — 85 versions: a9d4104f → 4110ba86 → 6e2dc3fd → 1f486539 → f10a08ba → 48886c98 → 6aeaf85c → ca0c09e0 → 130372c0 → f69680b3 → 50364568 → 30e6dee3 → 3bb6d4f8 → 26a25696 → 0951d355 → 3261f321 → 5a190e74 → 80762a78 → d2bd49f3 → 99859be7 → b134cecf → 713f693b → 5faa1d45 → 84b3d3aa → f0fd45d4 → a50c01c9 → a4b4526d → b6f981f1 → 93eeffff → d3feb512 → f21fe020 → cbe1a45d → d5146bbd → a33bb21f → a3f03a74 → 4e595599 → 4e595599 → 8e2b7886 → d16f38da → 2ed35267 → 4434dd90 → 91f0a37b → d9255ad2 → 29bd0903 → 0595f2de → d7b92bdf → 759fd310 → 24f2e69c → 64248c75 → 047adf17 → 777301ba → 6ac50997 → fc6f5af2 → 2b68d221 → 31c6221e → bbac3f70 → ffa19a63 → ab20f6c5 → b256ac1c → 063bdb3b → 2fd7556c → 9fcdf19c → 3aecde54 → ab28ddb8 → 11a22bcc → 5a8c1a67 → 970d3379 → aea28a26 → 1c34dc1f → 803237a3 → ef71d9bf → 35abfee7 → e18e4fc8 → 94df6f9c → 65adf86c → 65adf86c → 0a39af90 → 7a6b2e37 → c6e1a5ed → 1567d950 → 1f5e848e → 355d0be6 → 56ded035 → f75cb6b4 → a35e0f4a - `kei-registry::mini-prim` — 5 versions: 9fa2b304 → 9fa2b304 → 9fa2b304 → 9fa2b304 → 9fa2b304 @@ -1534,7 +1543,7 @@ Sorted alphabetically by name. - `kei-runtime-core` — 5 versions: 100eec0c → dedb3de0 → b9a37dea → 3ec878e2 → 3ec878e2dd71176a - `kei-runtime-core::kei-runtime-core` — 13 versions: 7980a704 → d64f3fbc → 9822303c → 80ad147f → ee80f871 → 663b5308 → 143c08b7 → ecfcc56c → 10186e32 → 0ace2c22 → a544e53a → 9c23c869 → 453db161 - `kei-runtime::kei-runtime` — 15 versions: e23e203b → 45e2bb3a → 93b703b3 → bd5a94ce → 15d85045 → 2aa2f1e3 → 2aa2f1e3 → 23f2ee6a → 37dc01f8 → bb9a2e8d → e013e322 → 70fd5389 → 67644265 → 4b3abe12 → 5fcf7642 -- `kei-sage` — 5 versions: 773af2fd → e7617e42 → 70873353 → d1c7d281 → 443fcc309d0cbaa1 +- `kei-sage` — 6 versions: 773af2fd → e7617e42 → 70873353 → d1c7d281 → 443fcc309d0cbaa1 → d1c7d2811c3b132d - `kei-sage::kei-sage` — 17 versions: df35dc55 → df35dc55 → 9ed33eef → 1ccf8553 → ace2ebe0 → 12f08988 → fdf01997 → 89230dfa → 412374dc → 412374dc → 526f83cf → 0aecbc7c → 953d2717 → 4219f080 → 9411c4d0 → 667bde03 → be5d29e2 - `kei-scheduler` — 5 versions: 589d4c96 → b20fdba2 → f1f1ebf8 → 71e42866 → 71e428667c0a51de - `kei-scheduler::kei-scheduler` — 2 versions: ef89066d → ef89066d @@ -1542,7 +1551,7 @@ Sorted alphabetically by name. - `kei-search-core::kei-search-core` — 9 versions: ff60e666 → ff60e666 → 96ff99a4 → 14e56266 → 320673de → 24303758 → 4c225682 → a1f36846 → cd51e70f - `kei-shared` — 5 versions: 5990b174 → c9abc1ac → 9effa42e → 881038bd → 881038bdfa81b0a8 - `kei-shared::kei-shared` — 8 versions: df6d9f3f → df6d9f3f → 24b821c9 → 04d318a6 → e74644f8 → cd44e72a → 92dbbe76 → 985486d6 -- `kei-skill-importer` — 5 versions: 18270170 → 8a09d39e → cb92de6f → 9a8f8225 → 9a8f8225093a7ce6 +- `kei-skill-importer` — 6 versions: 18270170 → 8a09d39e → cb92de6f → 9a8f8225 → 9a8f8225093a7ce6 → 96a13747f9a93260 - `kei-skill-importer::kei-skill-importer` — 4 versions: 99c79714 → 99c79714 → edb3646a → d5b46a57 - `kei-skills` — 5 versions: 0bc302bc → 9b27964c → 8b8fa1ed → 168eae70 → 168eae705265c03a - `kei-skills::kei-skills` — 2 versions: fa2242f8 → fa2242f8 @@ -1586,7 +1595,7 @@ Sorted alphabetically by name. - `output::report-format` — 3 versions: 2051e906 → 4da32467 → 4da32467db43d03c - `output::severity-grade` — 3 versions: ed37a6c0 → d58af2b1 → d58af2b1830e5753 - `output::verdict` — 2 versions: b7b8f09e → b7b8f09e3587d02c -- `phase-b-rem` — 7 versions: 69fdc9bc → df6af06f → 223c0c99 → 8545aba8 → 0698f19d → 65463582 → 65463582cf03e457 +- `phase-b-rem` — 8 versions: 69fdc9bc → df6af06f → 223c0c99 → 8545aba8 → 0698f19d → 65463582 → 65463582cf03e457 → 8545aba8d08ab7c1 - `phase-c-deep-sleep` — 3 versions: d6007c09 → 700a3c8d → 700a3c8d70f38e48 - `policy::git-ops-scope` — 2 versions: 4d43202c → 4d43202c9b9c901a - `policy::no-git-ops` — 3 versions: eed5a2d2 → 883d37bb → 883d37bbbc92efa1 @@ -1605,7 +1614,7 @@ Sorted alphabetically by name. - `scope::files-whitelist` — 3 versions: 5a2b126c → 20d7510d → 20d7510d5836e1d1 - `scope::read-only` — 2 versions: eeffc63a → eeffc63a66fad321 - `secrets-pre-guard` — 3 versions: 2025e90b → 95f8c30d → 95f8c30da586dea1 -- `session-end-dump` — 3 versions: 4909cdce → 4909cdce524fb70c → d73bcc22432312a6 +- `session-end-dump` — 4 versions: 4909cdce → 4909cdce524fb70c → d73bcc22432312a6 → 4909cdce524fb70c - `shipped-vs-functional::1-agent-self-tag-status-truth-marker` — 2 versions: b5ec90aa → 94f83554 - `shipped-vs-functional::2-hook-scan-claude-hooks-agent-stub-scan-sh` — 2 versions: 19866fb4 → 6c2a93d5 - `shipped-vs-functional::3-orchestrator-pre-commit-gate` — 2 versions: 1719fc7e → 06326b0a @@ -1613,7 +1622,7 @@ Sorted alphabetically by name. - `skill-record` — 4 versions: cdf67741 → e2444805 → 44e464fe → 44e464fe9e3d5881 - `sleep-report-tg` — 4 versions: acc3ebfb → ef101ab6 → 9529ec50 → 9529ec503aab1f2c - `ssh-check` — 4 versions: f419e2b0 → ebd97541 → efaf8856 → efaf88561df1143f -- `stop-verify` — 4 versions: ea57eb38 → 81f1dd9e → 10673c57 → 10673c572a6d504f +- `stop-verify` — 5 versions: ea57eb38 → 81f1dd9e → 10673c57 → 10673c572a6d504f → ea57eb3823f79ee6 - `task-timer` — 6 versions: 202823f9 → 16e4f0a3 → a48f5401 → 4482de6f → d1289992 → d12899927f89056f - `tokens-sync` — 4 versions: 54c149ab → 69857925 → 18793d64 → 18793d64c6cd18dc - `tomd-preread` — 2 versions: e2cec1bb → e2cec1bb46cb50bd